admin-ui-starter-kit 0.1.2 → 0.1.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/.agents/skills/admin-ui-consumer-migration/SKILL.md +126 -0
- package/.agents/skills/component-library-rules/SKILL.md +12 -2
- package/AGENTS.md +6 -4
- package/COMPONENT_SELECTION.md +115 -0
- package/INTEGRATION.md +6 -0
- package/MIGRATION.md +235 -0
- package/PUBLISHING.md +6 -1
- package/README.md +25 -6
- package/package.json +6 -1
- package/scripts/audit-consumer.mjs +326 -0
- package/scripts/install-skill.mjs +50 -36
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: admin-ui-consumer-migration
|
|
3
|
+
description: Use when migrating a React app to consume `admin-ui-starter-kit`, replacing copied local UI folders, auditing exact package imports, deciding what stays app-owned, or wiring framework adapters such as Inertia without recreating local package mirrors.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Admin UI Consumer Migration
|
|
7
|
+
|
|
8
|
+
Use this skill inside consuming apps, not when editing package internals.
|
|
9
|
+
|
|
10
|
+
Goal: move reusable UI to `admin-ui-starter-kit` imports while keeping
|
|
11
|
+
app-specific routing, APIs, auth, translations, brand assets, and domain logic
|
|
12
|
+
inside the app.
|
|
13
|
+
|
|
14
|
+
## First reads
|
|
15
|
+
|
|
16
|
+
When available in the installed package or repo, read these before editing:
|
|
17
|
+
|
|
18
|
+
1. `MIGRATION.md` — migration sequence and deletion rules.
|
|
19
|
+
2. `COMPONENT_SELECTION.md` — which component/import to use.
|
|
20
|
+
3. `INTEGRATION.md` — install, provider, exact import, and framework wiring.
|
|
21
|
+
4. `.agents/skills/component-library-rules/references/components/INDEX.json` —
|
|
22
|
+
searchable component index.
|
|
23
|
+
|
|
24
|
+
Do not bulk-load every component doc. Use the index, then read only the matching
|
|
25
|
+
component file.
|
|
26
|
+
|
|
27
|
+
## Migration order
|
|
28
|
+
|
|
29
|
+
1. Install package, stylesheet, and `<UIProvider>`.
|
|
30
|
+
2. Replace typography, buttons, badges, cards, and simple display components.
|
|
31
|
+
3. Replace repeated row and metadata patterns with `Item` and `MetadataList`.
|
|
32
|
+
4. Replace form rows with `FormField` / `ControlledFormField`.
|
|
33
|
+
5. Replace tables, filters, metrics, comments, overlays, and other features.
|
|
34
|
+
6. Replace page/header/sidebar layout surfaces last.
|
|
35
|
+
7. Delete copied local folders only after imports, typecheck, and build are
|
|
36
|
+
clean.
|
|
37
|
+
|
|
38
|
+
## Exact package imports only
|
|
39
|
+
|
|
40
|
+
Use:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
44
|
+
import { Text } from 'admin-ui-starter-kit/typography';
|
|
45
|
+
import { MetadataList } from 'admin-ui-starter-kit/base/display/metadata';
|
|
46
|
+
import { Comments } from 'admin-ui-starter-kit/features/comments';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Do not use:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { Button } from '@/components/ui/base/buttons';
|
|
53
|
+
import { Button } from 'admin-ui-starter-kit/base';
|
|
54
|
+
import { MetadataList } from 'admin-ui-starter-kit/base/display/metadata/metadata-list';
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If a path is not listed in `package.json` `exports`, treat it as private.
|
|
58
|
+
|
|
59
|
+
## Adapter rule
|
|
60
|
+
|
|
61
|
+
Adapters are allowed only when they bind app-specific behaviour.
|
|
62
|
+
|
|
63
|
+
Good:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { Link, router } from '@inertiajs/react';
|
|
67
|
+
import { Button, type ButtonProps } from 'admin-ui-starter-kit/base/buttons';
|
|
68
|
+
|
|
69
|
+
export function InertiaButton({ href, ...props }: ButtonProps & { href: string }) {
|
|
70
|
+
return <Button {...props} onClick={() => router.visit(href)} />;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Bad:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Do not create local mirrors, barrels, or aliases that hide package imports.
|
|
81
|
+
Agents should replace call-site imports directly unless a wrapper adds routing,
|
|
82
|
+
auth, API, translation, or domain behaviour.
|
|
83
|
+
|
|
84
|
+
## Keep app-owned
|
|
85
|
+
|
|
86
|
+
Do not migrate these into package imports or generic wrappers:
|
|
87
|
+
|
|
88
|
+
- Brand/logo assets and product marks.
|
|
89
|
+
- Error boundaries.
|
|
90
|
+
- Auth, permission gates, tenant/workspace selectors.
|
|
91
|
+
- Route-aware global search content and API-backed search.
|
|
92
|
+
- Domain-specific cards/features that encode business rules.
|
|
93
|
+
- Inertia/router/query clients, Wayfinder actions, API calls, app i18n.
|
|
94
|
+
- Page-level data loading and layout orchestration.
|
|
95
|
+
|
|
96
|
+
## Useful commands
|
|
97
|
+
|
|
98
|
+
Run in the consuming app:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx admin-ui-starter-kit-audit
|
|
102
|
+
npx admin-ui-starter-kit-audit --fix
|
|
103
|
+
npm run typecheck
|
|
104
|
+
npm run build
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `--fix` only for conservative import rewrites from known copied paths.
|
|
108
|
+
Review the diff before deleting local folders.
|
|
109
|
+
|
|
110
|
+
## Component selection shortcuts
|
|
111
|
+
|
|
112
|
+
| Need | Import |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| Text/headings | `admin-ui-starter-kit/typography` |
|
|
115
|
+
| Buttons | `admin-ui-starter-kit/base/buttons` |
|
|
116
|
+
| Badges | `admin-ui-starter-kit/base/badge` |
|
|
117
|
+
| Card shell | `admin-ui-starter-kit/base/cards` |
|
|
118
|
+
| Icon/avatar/title row | `admin-ui-starter-kit/base/item` |
|
|
119
|
+
| Label/value metadata | `admin-ui-starter-kit/base/display/metadata` |
|
|
120
|
+
| Form rows | `admin-ui-starter-kit/base/forms` |
|
|
121
|
+
| Tables | `admin-ui-starter-kit/base/table` |
|
|
122
|
+
| Filters | `admin-ui-starter-kit/features/filters` |
|
|
123
|
+
| Comments | `admin-ui-starter-kit/features/comments` |
|
|
124
|
+
| Dialogs/drawers | `admin-ui-starter-kit/features/overlays` |
|
|
125
|
+
| Metrics/charts | `admin-ui-starter-kit/composed/analytics` |
|
|
126
|
+
| Page/header/sidebar | `admin-ui-starter-kit/layout/page`, `/header`, `/sidebar` |
|
|
@@ -56,6 +56,11 @@ These deeper guides live alongside this file under `references/`. The skill stay
|
|
|
56
56
|
|
|
57
57
|
Don't read all of them — find the matching guide for the immediate task, follow it, return.
|
|
58
58
|
|
|
59
|
+
Consumer-app migration guidance lives outside this maintainer skill:
|
|
60
|
+
[`../admin-ui-consumer-migration/SKILL.md`](../admin-ui-consumer-migration/SKILL.md),
|
|
61
|
+
`MIGRATION.md`, and `COMPONENT_SELECTION.md`. Use those when replacing copied
|
|
62
|
+
local UI in downstream apps.
|
|
63
|
+
|
|
59
64
|
## Component reference index
|
|
60
65
|
|
|
61
66
|
Every component documented in the showcase has a generated markdown
|
|
@@ -99,9 +104,14 @@ to see how the library uses it before writing fresh code.
|
|
|
99
104
|
The references are generated from the MDX showcase by
|
|
100
105
|
`npm run docs:sync-skill`; CI runs the freshness check via `npm run verify`.
|
|
101
106
|
|
|
102
|
-
##
|
|
107
|
+
## Maintainer skill scope
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
This skill is the source of truth for anything inside `src/components/**`.
|
|
110
|
+
The separate `admin-ui-consumer-migration` skill is for downstream app
|
|
111
|
+
migrations. The harness may surface generic skills (`frontend-design`,
|
|
112
|
+
`shadcn`, `tailwind-v4-shadcn`, etc.) from the user's plugin set, but for this
|
|
113
|
+
package's component internals, this skill wins. The relevant material from
|
|
114
|
+
those generic skills is inlined here:
|
|
105
115
|
|
|
106
116
|
- **Visual taste / "escape generic AI aesthetics"** → see rule 16 (visual evaluation) and the "Tone for visual work" section in `AGENTS.md`. The library voice is calm, neutral, dense without being cramped — admin density, not marketing flash.
|
|
107
117
|
- **Adding a shadcn primitive** → step 0 of [`references/base-wrapper.md`](references/base-wrapper.md): `npx shadcn@latest add <primitive>` lands the file in `src/components/ui/<primitive>.tsx`. Then write the wrapper.
|
package/AGENTS.md
CHANGED
|
@@ -43,7 +43,7 @@ Read it once for context; it is not a live roadmap.
|
|
|
43
43
|
|
|
44
44
|
## Mandatory reading before any code change
|
|
45
45
|
|
|
46
|
-
The **only** mandatory skill in this repo is
|
|
46
|
+
The **only** mandatory maintainer skill in this repo is
|
|
47
47
|
[`component-library-rules`](.agents/skills/component-library-rules/SKILL.md).
|
|
48
48
|
It encodes 24 rules covering layer order, typography, density, tokens,
|
|
49
49
|
strings/i18n, framework-agnostic contracts, slot/render-prop
|
|
@@ -59,9 +59,11 @@ pages, ui-provider, consumer wiring, import paths, visual evaluation,
|
|
|
59
59
|
testing, layout, composed-domains. Pull the matching one when the work
|
|
60
60
|
fits; don't read all of them.
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
The repo also ships
|
|
63
|
+
[`admin-ui-consumer-migration`](.agents/skills/admin-ui-consumer-migration/SKILL.md)
|
|
64
|
+
for downstream apps migrating copied UI into the package. Use that skill in
|
|
65
|
+
consumer repos; use `component-library-rules` when changing this package.
|
|
66
|
+
If a topic isn't covered, ask before inventing a pattern.
|
|
65
67
|
|
|
66
68
|
## The architectural layers
|
|
67
69
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Component Selection Guide
|
|
2
|
+
|
|
3
|
+
Use this when deciding which package entrypoint belongs at a call site.
|
|
4
|
+
|
|
5
|
+
Public imports are exact. If a path is not listed in `package.json` `exports`
|
|
6
|
+
or documented here, treat it as private.
|
|
7
|
+
|
|
8
|
+
## Common decisions
|
|
9
|
+
|
|
10
|
+
| Need | Use | Import |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| Text, headings, labels, links | `Text`, `Heading`, `Label`, `TextLink` | `admin-ui-starter-kit/typography` |
|
|
13
|
+
| Button or icon button | `Button`, `TextButton`, button variants | `admin-ui-starter-kit/base/buttons` |
|
|
14
|
+
| Status pill or count badge | `Badge` | `admin-ui-starter-kit/base/badge` |
|
|
15
|
+
| Card shell | `SmartCard` | `admin-ui-starter-kit/base/cards` |
|
|
16
|
+
| Row with icon/avatar/title/meta/actions | `Item` family | `admin-ui-starter-kit/base/item` |
|
|
17
|
+
| Label/value metadata | `MetadataList` | `admin-ui-starter-kit/base/display/metadata` |
|
|
18
|
+
| Tooltip, avatar, separator, inline stat | display family | `admin-ui-starter-kit/base/display` |
|
|
19
|
+
| Form label/control/error row | `FormField`, `ControlledFormField` | `admin-ui-starter-kit/base/forms` |
|
|
20
|
+
| Inputs, selects, textareas, QR fields | form field exports | `admin-ui-starter-kit/base/forms` |
|
|
21
|
+
| Searchable select | `Combobox` | `admin-ui-starter-kit/base/combobox` |
|
|
22
|
+
| Popover menu or command menu | `PopoverMenu`, `Command` | `admin-ui-starter-kit/base/popover-menu`, `admin-ui-starter-kit/base/command` |
|
|
23
|
+
| Dropdown navigation menus | navigation family | `admin-ui-starter-kit/base/navigation` |
|
|
24
|
+
| Data table | `DataTable` | `admin-ui-starter-kit/base/table` |
|
|
25
|
+
| Date picker or date range picker | date picker family | `admin-ui-starter-kit/base/date-pickers` |
|
|
26
|
+
| Map or place autocomplete | map family | `admin-ui-starter-kit/base/map` |
|
|
27
|
+
| KPI cards and charts | analytics surfaces | `admin-ui-starter-kit/composed/analytics` |
|
|
28
|
+
| Commerce/order/product surfaces | commerce surfaces | `admin-ui-starter-kit/composed/commerce` |
|
|
29
|
+
| Timeline | timeline surfaces | `admin-ui-starter-kit/composed/timelines` |
|
|
30
|
+
| Comments panel | `Comments` | `admin-ui-starter-kit/features/comments` |
|
|
31
|
+
| Filter bar/facets | filters feature | `admin-ui-starter-kit/features/filters` |
|
|
32
|
+
| Dialog, drawer, alert dialog | overlays feature | `admin-ui-starter-kit/features/overlays` |
|
|
33
|
+
| Global search shell | global search feature | `admin-ui-starter-kit/features/global-search` |
|
|
34
|
+
| Mentions/typeahead | mentions or suggestions feature | `admin-ui-starter-kit/features/mentions`, `admin-ui-starter-kit/features/suggestions` |
|
|
35
|
+
| Rich text editor | TipTap-backed editor | `admin-ui-starter-kit/features/rich-text-editor` |
|
|
36
|
+
| Page title/actions/breadcrumb chrome | page layout | `admin-ui-starter-kit/layout/page` |
|
|
37
|
+
| Header shell/search/action slots | header layout | `admin-ui-starter-kit/layout/header` |
|
|
38
|
+
| Sidebar shell | sidebar layout | `admin-ui-starter-kit/layout/sidebar` |
|
|
39
|
+
|
|
40
|
+
## Decision tree
|
|
41
|
+
|
|
42
|
+
1. Is it plain user-facing text?
|
|
43
|
+
Use `typography`. Do not hand-roll `text-xs text-muted-foreground` spans.
|
|
44
|
+
|
|
45
|
+
2. Is it a primitive UI control or presentational wrapper?
|
|
46
|
+
Use `base`. Buttons, badges, cards, form fields, popovers, tables, maps,
|
|
47
|
+
display helpers, and item rows live there.
|
|
48
|
+
|
|
49
|
+
3. Is it a reusable domain-shaped surface built from base components?
|
|
50
|
+
Use `composed`. Metrics, commerce cards, admin rows, timeline surfaces, and
|
|
51
|
+
dense data-display blocks belong there.
|
|
52
|
+
|
|
53
|
+
4. Does it include feature state, slots, callbacks, or app-level behaviour?
|
|
54
|
+
Use `features`. Comments, filters, search, overlays, mentions, sync, and
|
|
55
|
+
rich text editor live there.
|
|
56
|
+
|
|
57
|
+
5. Is it page chrome?
|
|
58
|
+
Use `layout`. Keep routing and active-link state in the app.
|
|
59
|
+
|
|
60
|
+
6. Does it hardcode backend, auth, routing, app translations, or product
|
|
61
|
+
branding?
|
|
62
|
+
Keep it in the app. Wrap package components only where the wrapper binds
|
|
63
|
+
app-specific behaviour.
|
|
64
|
+
|
|
65
|
+
## Adapter examples
|
|
66
|
+
|
|
67
|
+
Use an app adapter when you bind framework behaviour:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { router } from '@inertiajs/react';
|
|
71
|
+
import { Button, type ButtonProps } from 'admin-ui-starter-kit/base/buttons';
|
|
72
|
+
|
|
73
|
+
export function InertiaActionButton({
|
|
74
|
+
href,
|
|
75
|
+
method = 'get',
|
|
76
|
+
...props
|
|
77
|
+
}: ButtonProps & { href: string; method?: 'get' | 'post' | 'put' | 'delete' }) {
|
|
78
|
+
return <Button {...props} onClick={() => router.visit(href, { method })} />;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Do not create app wrappers that only rename or re-export package components:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
export { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Exact import rules
|
|
89
|
+
|
|
90
|
+
Use:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
94
|
+
import { Text } from 'admin-ui-starter-kit/typography';
|
|
95
|
+
import { MetadataList } from 'admin-ui-starter-kit/base/display/metadata';
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Do not use:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { Button } from 'admin-ui-starter-kit/base';
|
|
102
|
+
import { MetadataList } from 'admin-ui-starter-kit/base/display/metadata/metadata-list';
|
|
103
|
+
import { Button } from '@/components/ui/base/buttons';
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Where agents should look
|
|
107
|
+
|
|
108
|
+
1. `AGENTS.md`
|
|
109
|
+
2. `.agents/skills/component-library-rules/SKILL.md`
|
|
110
|
+
3. `.agents/skills/admin-ui-consumer-migration/SKILL.md`
|
|
111
|
+
4. `.agents/skills/component-library-rules/references/components/INDEX.json`
|
|
112
|
+
5. The specific component doc under `references/components/*.md`
|
|
113
|
+
6. `INTEGRATION.md`
|
|
114
|
+
7. `MIGRATION.md`
|
|
115
|
+
8. `package.json` `exports`
|
package/INTEGRATION.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Use this when installing `admin-ui-starter-kit` into a React app.
|
|
4
4
|
|
|
5
|
+
If the app already has a copied local design system, follow
|
|
6
|
+
[`MIGRATION.md`](MIGRATION.md) first. Use
|
|
7
|
+
[`COMPONENT_SELECTION.md`](COMPONENT_SELECTION.md) when deciding which exact
|
|
8
|
+
entrypoint replaces a local component.
|
|
9
|
+
|
|
5
10
|
## 1. Install the package and required peers
|
|
6
11
|
|
|
7
12
|
```bash
|
|
@@ -140,6 +145,7 @@ For a quick smoke test:
|
|
|
140
145
|
```bash
|
|
141
146
|
npm run typecheck
|
|
142
147
|
npm run build
|
|
148
|
+
npx admin-ui-starter-kit-audit
|
|
143
149
|
```
|
|
144
150
|
|
|
145
151
|
If a build reports a missing optional peer, either install the peer listed
|
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Migration Guide
|
|
2
|
+
|
|
3
|
+
Use this when replacing a copied local design system with the published
|
|
4
|
+
`admin-ui-starter-kit` package.
|
|
5
|
+
|
|
6
|
+
The goal is not to move every UI file into npm. The package owns reusable UI.
|
|
7
|
+
The consuming app owns routing, API calls, auth context, translations, brand
|
|
8
|
+
assets, and domain-specific behaviour.
|
|
9
|
+
|
|
10
|
+
## 1. Install and mount
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install admin-ui-starter-kit react react-dom tailwindcss lucide-react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If the app still uses `lucide-react` v0, upgrade it as part of the migration.
|
|
17
|
+
This package has a lucide v1 peer.
|
|
18
|
+
|
|
19
|
+
Mount the stylesheet and provider once:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import 'admin-ui-starter-kit/style.css';
|
|
23
|
+
import { UIProvider } from 'admin-ui-starter-kit/ui-provider';
|
|
24
|
+
|
|
25
|
+
<UIProvider
|
|
26
|
+
config={{
|
|
27
|
+
money: { defaultCurrency: 'USD', locale: 'en-US' },
|
|
28
|
+
dates: { weekStartsOn: 1 },
|
|
29
|
+
badge: { defaultSize: 'xs' },
|
|
30
|
+
button: { defaultSize: 'sm' },
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<App />
|
|
34
|
+
</UIProvider>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 2. Replace local primitives first
|
|
38
|
+
|
|
39
|
+
Start with the low-level imports used everywhere. Keep the changes mechanical
|
|
40
|
+
and verify after each group.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
44
|
+
import { Badge } from 'admin-ui-starter-kit/base/badge';
|
|
45
|
+
import { SmartCard } from 'admin-ui-starter-kit/base/cards';
|
|
46
|
+
import { Text, Heading } from 'admin-ui-starter-kit/typography';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Do not create local mirror files that re-export package primitives. Replace the
|
|
50
|
+
call-site imports directly.
|
|
51
|
+
|
|
52
|
+
## 3. Replace repeated display patterns
|
|
53
|
+
|
|
54
|
+
Use package components for shapes that repeat across the app:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { MetadataList } from 'admin-ui-starter-kit/base/display/metadata';
|
|
58
|
+
import { Item, ItemContent, ItemMedia, ItemTitle } from 'admin-ui-starter-kit/base/item';
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `MetadataList` for label/value data, compact summaries, and entity details.
|
|
62
|
+
Use `Item` for rows with media, title, description, metadata, or actions.
|
|
63
|
+
|
|
64
|
+
## 4. Replace form rows
|
|
65
|
+
|
|
66
|
+
Use the package form shell before moving larger forms:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { ControlledFormField, Input, Select } from 'admin-ui-starter-kit/base/forms';
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Keep form submission, validation schemas, and API mutation wiring in the app.
|
|
73
|
+
The package should own the field row and control presentation, not the backend
|
|
74
|
+
contract.
|
|
75
|
+
|
|
76
|
+
## 5. Replace tables, filters, metrics, and overlays
|
|
77
|
+
|
|
78
|
+
Move shared app surfaces after primitives are stable:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { DataTable } from 'admin-ui-starter-kit/base/table';
|
|
82
|
+
import { FilterProvider } from 'admin-ui-starter-kit/features/filters';
|
|
83
|
+
import { MetricGrid } from 'admin-ui-starter-kit/composed/analytics';
|
|
84
|
+
import { Dialog, Drawer } from 'admin-ui-starter-kit/features/overlays';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Install optional peers only for the entrypoints you import. For example,
|
|
88
|
+
`base/table` needs `@tanstack/react-table`, `composed/analytics` needs
|
|
89
|
+
`recharts`, and `features/rich-text-editor` needs TipTap.
|
|
90
|
+
|
|
91
|
+
## 6. Replace layouts last
|
|
92
|
+
|
|
93
|
+
Migrate page chrome after shared atoms and features are clean:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { PageHeader, PageActions } from 'admin-ui-starter-kit/layout/page';
|
|
97
|
+
import { Header, HeaderSearch } from 'admin-ui-starter-kit/layout/header';
|
|
98
|
+
import { AppSidebar, SidebarProvider } from 'admin-ui-starter-kit/layout/sidebar';
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Route-aware layout state, active navigation logic, auth menus, and application
|
|
102
|
+
search content stay in the app. The package provides the shell and slots.
|
|
103
|
+
|
|
104
|
+
## 7. Keep app-owned adapters, not local mirrors
|
|
105
|
+
|
|
106
|
+
A consuming app may create adapters for framework wiring, but not local mirrors.
|
|
107
|
+
|
|
108
|
+
Good:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { Link } from '@inertiajs/react';
|
|
112
|
+
import { PageHeader, type PageHeaderProps } from 'admin-ui-starter-kit/layout/page';
|
|
113
|
+
|
|
114
|
+
export function AppPageHeader(props: PageHeaderProps) {
|
|
115
|
+
return (
|
|
116
|
+
<PageHeader
|
|
117
|
+
{...props}
|
|
118
|
+
renderLink={(href, children) => <Link href={href}>{children}</Link>}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Bad:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
export { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
128
|
+
export { Text } from 'admin-ui-starter-kit/typography';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The package owns UI. The app owns routing, API calls, auth context, and
|
|
132
|
+
translations.
|
|
133
|
+
|
|
134
|
+
## 8. Inertia recipe
|
|
135
|
+
|
|
136
|
+
Keep Inertia imports in the consuming app:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { Link, router } from '@inertiajs/react';
|
|
140
|
+
import { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
141
|
+
import { Comments } from 'admin-ui-starter-kit/features/comments';
|
|
142
|
+
import { PageHeader } from 'admin-ui-starter-kit/layout/page';
|
|
143
|
+
|
|
144
|
+
<PageHeader
|
|
145
|
+
title="Customers"
|
|
146
|
+
actions={<Button onClick={() => router.visit('/customers/create')}>Create</Button>}
|
|
147
|
+
renderLink={(href, children) => <Link href={href}>{children}</Link>}
|
|
148
|
+
/>;
|
|
149
|
+
|
|
150
|
+
<Comments
|
|
151
|
+
comments={comments}
|
|
152
|
+
onSubmit={(values) => router.post(route('comments.store'), values)}
|
|
153
|
+
onDelete={(id) => router.delete(route('comments.destroy', id))}
|
|
154
|
+
strings={{
|
|
155
|
+
empty: t('comments.empty'),
|
|
156
|
+
composerPlaceholder: t('comments.placeholder'),
|
|
157
|
+
}}
|
|
158
|
+
/>;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Do not add Inertia adapters to the package. Document app adapters locally when
|
|
162
|
+
they bind routes, Wayfinder actions, translations, or app APIs.
|
|
163
|
+
|
|
164
|
+
## 9. Do not migrate these
|
|
165
|
+
|
|
166
|
+
Keep these in the consuming app:
|
|
167
|
+
|
|
168
|
+
- Brand assets, logos, app icons, and product-specific marks.
|
|
169
|
+
- Error boundaries and app-level exception handling.
|
|
170
|
+
- Route-aware search content and backend-connected global search.
|
|
171
|
+
- Auth menus, permission gates, and tenant/workspace selectors.
|
|
172
|
+
- Domain-specific cards and features that encode business rules.
|
|
173
|
+
- API clients, query caches, Inertia router calls, and Wayfinder route actions.
|
|
174
|
+
- App-specific layout orchestration and page-level data loading.
|
|
175
|
+
|
|
176
|
+
## 10. Delete copied folders after imports are clean
|
|
177
|
+
|
|
178
|
+
Only delete local copied folders after typecheck and build pass:
|
|
179
|
+
|
|
180
|
+
```text
|
|
181
|
+
components/ui/base
|
|
182
|
+
components/ui/typography
|
|
183
|
+
components/ui/navigation
|
|
184
|
+
components/layout
|
|
185
|
+
components/features/comments
|
|
186
|
+
components/features/filters
|
|
187
|
+
components/features/metrics
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The exact folders vary by app. Delete only copied package equivalents; keep
|
|
191
|
+
app-owned components.
|
|
192
|
+
|
|
193
|
+
## 11. Hard import rules
|
|
194
|
+
|
|
195
|
+
Use exact public exports:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
import { Button } from 'admin-ui-starter-kit/base/buttons';
|
|
199
|
+
import { Text } from 'admin-ui-starter-kit/typography';
|
|
200
|
+
import { MetadataList } from 'admin-ui-starter-kit/base/display/metadata';
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Do not use:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { Button } from '@/components/ui/base/buttons';
|
|
207
|
+
import { Button } from 'admin-ui-starter-kit/base';
|
|
208
|
+
import { MetadataList } from 'admin-ui-starter-kit/base/display/metadata/metadata-list';
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Run the package audit in a consumer app:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npx admin-ui-starter-kit-audit
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Use `--fix` for conservative import rewrites from known copied paths:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npx admin-ui-starter-kit-audit --fix
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 12. Verification
|
|
224
|
+
|
|
225
|
+
After each migration chunk:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npm run typecheck
|
|
229
|
+
npm run build
|
|
230
|
+
npx admin-ui-starter-kit-audit
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
If a package import fails, check `package.json` `exports` in
|
|
234
|
+
`admin-ui-starter-kit`. Public imports are exact keys. Undocumented nested
|
|
235
|
+
paths are private even when declaration files exist under `dist/`.
|
package/PUBLISHING.md
CHANGED
|
@@ -54,7 +54,12 @@ Read the file list. **Confirm:**
|
|
|
54
54
|
- `dist/files/` and `dist/images/` are included for font and map CSS assets.
|
|
55
55
|
- `style.css.d.ts` is included for strict TypeScript side-effect CSS imports.
|
|
56
56
|
- `dist/showcase/` is included.
|
|
57
|
-
- `README.md`, `INTEGRATION.md`, `
|
|
57
|
+
- `README.md`, `INTEGRATION.md`, `MIGRATION.md`,
|
|
58
|
+
`COMPONENT_SELECTION.md`, `LICENSE`, `CHANGELOG.md` are included.
|
|
59
|
+
- `.agents/skills/component-library-rules/` and
|
|
60
|
+
`.agents/skills/admin-ui-consumer-migration/` are included.
|
|
61
|
+
- `scripts/audit-consumer.mjs`, `scripts/install-skill.mjs`, and
|
|
62
|
+
`scripts/serve-showcase.mjs` are included.
|
|
58
63
|
- `src/App.css` is included so consumers can copy the token source when
|
|
59
64
|
building their own Tailwind output.
|
|
60
65
|
- `node_modules/`, `coverage/`, `.git/`, `dist-ssr/`, the preview app
|
package/README.md
CHANGED
|
@@ -99,6 +99,11 @@ exactly your tool.**
|
|
|
99
99
|
For a full framework and bundler setup walkthrough, see
|
|
100
100
|
[INTEGRATION.md](INTEGRATION.md).
|
|
101
101
|
|
|
102
|
+
Migrating an app that already copied an older local UI tree? Start with
|
|
103
|
+
[MIGRATION.md](MIGRATION.md), then use
|
|
104
|
+
[COMPONENT_SELECTION.md](COMPONENT_SELECTION.md) to choose exact package
|
|
105
|
+
entrypoints.
|
|
106
|
+
|
|
102
107
|
### 1. Add the package
|
|
103
108
|
|
|
104
109
|
```bash
|
|
@@ -164,23 +169,37 @@ import { UIProvider } from 'admin-ui-starter-kit/ui-provider';
|
|
|
164
169
|
|
|
165
170
|
The provider is locked at first mount; pass everything once at the root.
|
|
166
171
|
|
|
167
|
-
### 6. Install the AI agent
|
|
172
|
+
### 6. Install the AI agent skills (optional)
|
|
173
|
+
|
|
174
|
+
This package ships AI skills for Claude Code and other agents:
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
- `component-library-rules` — maintainer rules for editing this library.
|
|
177
|
+
- `admin-ui-consumer-migration` — consumer-app migration rules and guardrails.
|
|
178
|
+
|
|
179
|
+
After installing the package:
|
|
171
180
|
|
|
172
181
|
```bash
|
|
173
182
|
npx admin-ui-starter-kit-install-skill
|
|
174
183
|
```
|
|
175
184
|
|
|
176
|
-
Copies
|
|
177
|
-
|
|
178
|
-
package upgrade.
|
|
185
|
+
Copies both skills into your project's `.claude/skills/` and
|
|
186
|
+
`.agents/skills/`. Run again to update after a package upgrade.
|
|
179
187
|
|
|
180
188
|
Flags:
|
|
189
|
+
- `--skill=all|component-library-rules|admin-ui-consumer-migration` (default `all`)
|
|
181
190
|
- `--target=claude|agents|both` (default `both`) — pick which directory to install into.
|
|
182
191
|
- `--force` — overwrite without prompting.
|
|
183
192
|
|
|
193
|
+
For migration checks in a consuming app:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npx admin-ui-starter-kit-audit
|
|
197
|
+
npx admin-ui-starter-kit-audit --fix
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The audit rejects stale copied imports, invalid package subpaths, and local
|
|
201
|
+
files that only re-export package components.
|
|
202
|
+
|
|
184
203
|
---
|
|
185
204
|
|
|
186
205
|
## Browse the component showcase
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "admin-ui-starter-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Opinionated React component kit for admin panels and SaaS dashboards, built on top of shadcn/ui + Tailwind CSS v4. Callback-driven, framework-agnostic, i18n-ready.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
31
|
"README.md",
|
|
32
|
+
"MIGRATION.md",
|
|
33
|
+
"COMPONENT_SELECTION.md",
|
|
32
34
|
"INTEGRATION.md",
|
|
33
35
|
"CHANGELOG.md",
|
|
34
36
|
"CONTRIBUTING.md",
|
|
@@ -39,10 +41,13 @@
|
|
|
39
41
|
"CLAUDE.md",
|
|
40
42
|
"AGENTS.md",
|
|
41
43
|
".agents/skills/component-library-rules",
|
|
44
|
+
".agents/skills/admin-ui-consumer-migration",
|
|
45
|
+
"scripts/audit-consumer.mjs",
|
|
42
46
|
"scripts/install-skill.mjs",
|
|
43
47
|
"scripts/serve-showcase.mjs"
|
|
44
48
|
],
|
|
45
49
|
"bin": {
|
|
50
|
+
"admin-ui-starter-kit-audit": "./scripts/audit-consumer.mjs",
|
|
46
51
|
"admin-ui-starter-kit-install-skill": "./scripts/install-skill.mjs",
|
|
47
52
|
"admin-ui-starter-kit-showcase": "./scripts/serve-showcase.mjs"
|
|
48
53
|
},
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* audit-consumer — checks a consuming app for common admin-ui-starter-kit
|
|
4
|
+
* migration mistakes:
|
|
5
|
+
*
|
|
6
|
+
* - stale copied import paths such as @/components/ui/base/buttons
|
|
7
|
+
* - invalid package imports not listed in package.json exports
|
|
8
|
+
* - local files that only re-export package components
|
|
9
|
+
*
|
|
10
|
+
* Run from a consumer app:
|
|
11
|
+
*
|
|
12
|
+
* npx admin-ui-starter-kit-audit
|
|
13
|
+
* npx admin-ui-starter-kit-audit --fix
|
|
14
|
+
*/
|
|
15
|
+
import { promises as fs } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const PACKAGE_NAME = 'admin-ui-starter-kit';
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..');
|
|
22
|
+
const PACKAGE_JSON = JSON.parse(
|
|
23
|
+
await fs.readFile(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'),
|
|
24
|
+
);
|
|
25
|
+
const PUBLIC_EXPORTS = new Set(Object.keys(PACKAGE_JSON.exports ?? {}));
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PATHS = ['src', 'app', 'resources/js', 'components', 'pages'];
|
|
28
|
+
const CODE_EXTENSIONS = new Set(['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']);
|
|
29
|
+
const SKIP_DIRS = new Set([
|
|
30
|
+
'.git',
|
|
31
|
+
'.next',
|
|
32
|
+
'.nuxt',
|
|
33
|
+
'.turbo',
|
|
34
|
+
'.vercel',
|
|
35
|
+
'build',
|
|
36
|
+
'coverage',
|
|
37
|
+
'dist',
|
|
38
|
+
'node_modules',
|
|
39
|
+
'public',
|
|
40
|
+
'vendor',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const opts = { fix: false, json: false, help: false, paths: [] };
|
|
45
|
+
for (const arg of argv.slice(2)) {
|
|
46
|
+
if (arg === '--fix') {
|
|
47
|
+
opts.fix = true;
|
|
48
|
+
} else if (arg === '--json') {
|
|
49
|
+
opts.json = true;
|
|
50
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
51
|
+
opts.help = true;
|
|
52
|
+
} else if (arg.startsWith('--paths=')) {
|
|
53
|
+
opts.paths.push(...arg.slice('--paths='.length).split(',').filter(Boolean));
|
|
54
|
+
} else if (arg.startsWith('-')) {
|
|
55
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
56
|
+
} else {
|
|
57
|
+
opts.paths.push(arg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return opts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printHelp() {
|
|
64
|
+
console.log(
|
|
65
|
+
`Usage: npx ${PACKAGE_NAME}-audit [options] [paths...]\n\n` +
|
|
66
|
+
`Audits a consuming app for stale local UI imports, invalid package subpaths,\n` +
|
|
67
|
+
`and local files that only mirror ${PACKAGE_NAME} exports.\n\n` +
|
|
68
|
+
`Options:\n` +
|
|
69
|
+
` --fix Rewrite known copied import paths to package imports\n` +
|
|
70
|
+
` --paths=a,b Comma-separated paths to scan\n` +
|
|
71
|
+
` --json Print machine-readable JSON\n` +
|
|
72
|
+
` --help, -h Show this message\n`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function exists(p) {
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(p);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function walk(dir, files = []) {
|
|
86
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
89
|
+
const full = path.join(dir, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
await walk(full, files);
|
|
92
|
+
} else if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
93
|
+
files.push(full);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getScanFiles(cwd, requestedPaths) {
|
|
100
|
+
const roots = requestedPaths.length > 0 ? requestedPaths : DEFAULT_PATHS;
|
|
101
|
+
const files = [];
|
|
102
|
+
for (const relative of roots) {
|
|
103
|
+
const full = path.resolve(cwd, relative);
|
|
104
|
+
if (!(await exists(full))) continue;
|
|
105
|
+
const stat = await fs.stat(full);
|
|
106
|
+
if (stat.isDirectory()) {
|
|
107
|
+
await walk(full, files);
|
|
108
|
+
} else if (stat.isFile() && CODE_EXTENSIONS.has(path.extname(full))) {
|
|
109
|
+
files.push(full);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return [...new Set(files)].sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getImportSpecifiers(source) {
|
|
116
|
+
const specifiers = [];
|
|
117
|
+
const patterns = [
|
|
118
|
+
/\bimport\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/g,
|
|
119
|
+
/\bexport\s+(?:type\s+)?[^'"]*?\s+from\s+['"]([^'"]+)['"]/g,
|
|
120
|
+
/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
121
|
+
];
|
|
122
|
+
for (const pattern of patterns) {
|
|
123
|
+
for (const match of source.matchAll(pattern)) specifiers.push(match[1]);
|
|
124
|
+
}
|
|
125
|
+
return specifiers;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizePackageKey(specifier) {
|
|
129
|
+
if (specifier === PACKAGE_NAME) return '.';
|
|
130
|
+
if (!specifier.startsWith(`${PACKAGE_NAME}/`)) return null;
|
|
131
|
+
return `.${specifier.slice(PACKAGE_NAME.length)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function packageImportIssue(specifier) {
|
|
135
|
+
const key = normalizePackageKey(specifier);
|
|
136
|
+
if (key === null) return null;
|
|
137
|
+
if (PUBLIC_EXPORTS.has(key)) return null;
|
|
138
|
+
return {
|
|
139
|
+
kind: 'invalid-package-import',
|
|
140
|
+
specifier,
|
|
141
|
+
message: `${specifier} is not a public ${PACKAGE_NAME} export`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function mapOldSpecifier(specifier) {
|
|
146
|
+
const typographyPrefixes = [
|
|
147
|
+
'@/components/ui/base/typography',
|
|
148
|
+
'@/components/ui/typography',
|
|
149
|
+
'@/components/typography',
|
|
150
|
+
];
|
|
151
|
+
if (typographyPrefixes.some((prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`))) {
|
|
152
|
+
return `${PACKAGE_NAME}/typography`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const layerMappings = [
|
|
156
|
+
{ prefix: '@/components/ui/base/', target: 'base' },
|
|
157
|
+
{ prefix: '@/components/base/', target: 'base' },
|
|
158
|
+
{ prefix: '@/components/composed/', target: 'composed' },
|
|
159
|
+
{ prefix: '@/components/features/', target: 'features' },
|
|
160
|
+
{ prefix: '@/components/layout/', target: 'layout' },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
for (const mapping of layerMappings) {
|
|
164
|
+
if (!specifier.startsWith(mapping.prefix)) continue;
|
|
165
|
+
const rest = specifier.slice(mapping.prefix.length);
|
|
166
|
+
const parts = rest.split('/').filter(Boolean);
|
|
167
|
+
if (parts.length === 0) continue;
|
|
168
|
+
|
|
169
|
+
if (mapping.target === 'base' && parts[0] === 'display' && parts[1] === 'metadata') {
|
|
170
|
+
return `${PACKAGE_NAME}/base/display/metadata`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const candidate = `${PACKAGE_NAME}/${mapping.target}/${parts[0]}`;
|
|
174
|
+
if (PUBLIC_EXPORTS.has(normalizePackageKey(candidate))) return candidate;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (specifier === '@/components/layout') return `${PACKAGE_NAME}/layout`;
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isKnownCopiedImport(specifier) {
|
|
183
|
+
return mapOldSpecifier(specifier) !== null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isMirrorFile(source) {
|
|
187
|
+
const withoutComments = source
|
|
188
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
189
|
+
.replace(/^\s*\/\/.*$/gm, '')
|
|
190
|
+
.trim();
|
|
191
|
+
if (!withoutComments) return false;
|
|
192
|
+
|
|
193
|
+
const statements = withoutComments
|
|
194
|
+
.split(';')
|
|
195
|
+
.map((statement) => statement.trim())
|
|
196
|
+
.filter(Boolean);
|
|
197
|
+
if (statements.length === 0) return false;
|
|
198
|
+
|
|
199
|
+
return statements.every((statement) =>
|
|
200
|
+
/^export\s+(?:type\s+)?(?:\{[\s\S]*\}|\*)\s+from\s+['"]admin-ui-starter-kit(?:\/[^'"]+)?['"]$/m.test(
|
|
201
|
+
statement,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function replaceSpecifiers(source) {
|
|
207
|
+
let changed = false;
|
|
208
|
+
const next = source.replace(
|
|
209
|
+
/(['"])(@\/components\/(?:ui\/base|ui\/typography|base|typography|composed|features|layout)(?:\/[^'"]*)?)\1/g,
|
|
210
|
+
(match, quote, specifier) => {
|
|
211
|
+
const replacement = mapOldSpecifier(specifier);
|
|
212
|
+
if (!replacement) return match;
|
|
213
|
+
changed = true;
|
|
214
|
+
return `${quote}${replacement}${quote}`;
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
return { changed, source: next };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function audit(opts) {
|
|
221
|
+
const cwd = process.cwd();
|
|
222
|
+
const files = await getScanFiles(cwd, opts.paths);
|
|
223
|
+
const issues = [];
|
|
224
|
+
const fixedFiles = [];
|
|
225
|
+
|
|
226
|
+
for (const file of files) {
|
|
227
|
+
const source = await fs.readFile(file, 'utf8');
|
|
228
|
+
const relative = path.relative(cwd, file);
|
|
229
|
+
const specifiers = getImportSpecifiers(source);
|
|
230
|
+
|
|
231
|
+
for (const specifier of specifiers) {
|
|
232
|
+
if (isKnownCopiedImport(specifier)) {
|
|
233
|
+
issues.push({
|
|
234
|
+
kind: 'stale-local-import',
|
|
235
|
+
file: relative,
|
|
236
|
+
specifier,
|
|
237
|
+
suggested: mapOldSpecifier(specifier),
|
|
238
|
+
message: `${specifier} should import directly from ${PACKAGE_NAME}`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const invalidPackageImport = packageImportIssue(specifier);
|
|
243
|
+
if (invalidPackageImport) {
|
|
244
|
+
issues.push({ ...invalidPackageImport, file: relative });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isMirrorFile(source)) {
|
|
249
|
+
issues.push({
|
|
250
|
+
kind: 'local-package-mirror',
|
|
251
|
+
file: relative,
|
|
252
|
+
message: 'local file only re-exports admin-ui-starter-kit; replace call-site imports directly',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (opts.fix) {
|
|
257
|
+
const replacement = replaceSpecifiers(source);
|
|
258
|
+
if (replacement.changed) {
|
|
259
|
+
await fs.writeFile(file, replacement.source);
|
|
260
|
+
fixedFiles.push(relative);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { filesScanned: files.length, issues, fixedFiles: [...new Set(fixedFiles)].sort() };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function printReport(result, opts) {
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
console.log(JSON.stringify(result, null, 2));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (result.fixedFiles.length > 0) {
|
|
275
|
+
console.log(`Rewrote imports in ${result.fixedFiles.length} file(s):`);
|
|
276
|
+
for (const file of result.fixedFiles) console.log(` - ${file}`);
|
|
277
|
+
console.log('');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (result.issues.length === 0) {
|
|
281
|
+
console.log(`Admin UI audit passed (${result.filesScanned} files scanned).`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.error(`Admin UI audit found ${result.issues.length} issue(s):`);
|
|
286
|
+
for (const issue of result.issues) {
|
|
287
|
+
const suggestion = issue.suggested ? ` -> ${issue.suggested}` : '';
|
|
288
|
+
console.error(`- ${issue.file}: ${issue.message}${suggestion}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!opts.fix) {
|
|
292
|
+
console.error('\nRun with --fix to rewrite known copied import paths.');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function main() {
|
|
297
|
+
const opts = parseArgs(process.argv);
|
|
298
|
+
if (opts.help) {
|
|
299
|
+
printHelp();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const first = await audit(opts);
|
|
304
|
+
if (!opts.fix || first.fixedFiles.length === 0) {
|
|
305
|
+
printReport(first, opts);
|
|
306
|
+
process.exit(first.issues.length > 0 ? 1 : 0);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const second = await audit({ ...opts, fix: false });
|
|
310
|
+
const merged = {
|
|
311
|
+
filesScanned: second.filesScanned,
|
|
312
|
+
issues: second.issues,
|
|
313
|
+
fixedFiles: first.fixedFiles,
|
|
314
|
+
};
|
|
315
|
+
printReport(merged, opts);
|
|
316
|
+
process.exit(second.issues.length > 0 ? 1 : 0);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main().catch((error) => {
|
|
320
|
+
if (error instanceof Error) {
|
|
321
|
+
console.error(`admin-ui-starter-kit-audit: ${error.message}`);
|
|
322
|
+
} else {
|
|
323
|
+
console.error(error);
|
|
324
|
+
}
|
|
325
|
+
process.exit(2);
|
|
326
|
+
});
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* install-skill — consumer-facing installer for the
|
|
4
|
-
* `
|
|
5
|
-
* `admin-ui-starter-kit` npm package.
|
|
3
|
+
* install-skill — consumer-facing installer for the AI skills that ship
|
|
4
|
+
* inside the `admin-ui-starter-kit` npm package.
|
|
6
5
|
*
|
|
7
6
|
* Run from a consumer project (after `npm install admin-ui-starter-kit`):
|
|
8
7
|
*
|
|
9
|
-
* npx admin-ui-starter-kit-install-skill [--target=claude|agents|both] [--force]
|
|
8
|
+
* npx admin-ui-starter-kit-install-skill [--skill=name|all] [--target=claude|agents|both] [--force]
|
|
10
9
|
*
|
|
11
10
|
* Default behaviour: copies the skill into BOTH
|
|
12
|
-
* <cwd>/.claude/skills
|
|
13
|
-
* <cwd>/.agents/skills
|
|
11
|
+
* <cwd>/.claude/skills/<skill-name>/
|
|
12
|
+
* <cwd>/.agents/skills/<skill-name>/
|
|
14
13
|
*
|
|
15
14
|
* The script is idempotent. It refuses to run inside the library itself
|
|
16
15
|
* (the maintainer has `scripts/publish-skill.mjs` for that workflow).
|
|
@@ -20,18 +19,16 @@ import path from 'node:path';
|
|
|
20
19
|
import readline from 'node:readline';
|
|
21
20
|
import { fileURLToPath } from 'node:url';
|
|
22
21
|
|
|
23
|
-
const
|
|
22
|
+
const SKILL_NAMES = ['component-library-rules', 'admin-ui-consumer-migration'];
|
|
24
23
|
const PACKAGE_NAME = 'admin-ui-starter-kit';
|
|
25
24
|
|
|
26
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
26
|
// scripts/install-skill.mjs lives at <pkg>/scripts/, so package root is one up.
|
|
28
27
|
const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..');
|
|
29
|
-
const SOURCE = path.join(PACKAGE_ROOT, '.agents', 'skills', SKILL_NAME);
|
|
30
|
-
|
|
31
28
|
const CWD = process.cwd();
|
|
32
29
|
|
|
33
30
|
function parseArgs(argv) {
|
|
34
|
-
const opts = { target: 'both', force: false, help: false };
|
|
31
|
+
const opts = { target: 'both', skill: 'all', force: false, help: false };
|
|
35
32
|
for (const arg of argv.slice(2)) {
|
|
36
33
|
if (arg === '--force' || arg === '-f') {
|
|
37
34
|
opts.force = true;
|
|
@@ -44,6 +41,15 @@ function parseArgs(argv) {
|
|
|
44
41
|
process.exit(2);
|
|
45
42
|
}
|
|
46
43
|
opts.target = value;
|
|
44
|
+
} else if (arg.startsWith('--skill=')) {
|
|
45
|
+
const value = arg.slice('--skill='.length);
|
|
46
|
+
if (value !== 'all' && !SKILL_NAMES.includes(value)) {
|
|
47
|
+
console.error(
|
|
48
|
+
`✖ Invalid --skill=${value}. Expected one of: all, ${SKILL_NAMES.join(', ')}.`,
|
|
49
|
+
);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
opts.skill = value;
|
|
47
53
|
} else {
|
|
48
54
|
console.error(`✖ Unknown argument: ${arg}`);
|
|
49
55
|
opts.help = true;
|
|
@@ -55,8 +61,9 @@ function parseArgs(argv) {
|
|
|
55
61
|
function printHelp() {
|
|
56
62
|
console.log(
|
|
57
63
|
`Usage: npx ${PACKAGE_NAME}-install-skill [options]\n\n` +
|
|
58
|
-
`Installs
|
|
64
|
+
`Installs ${PACKAGE_NAME} AI skills into the current project.\n\n` +
|
|
59
65
|
`Options:\n` +
|
|
66
|
+
` --skill=all|${SKILL_NAMES.join('|')} Which skill to install (default: all)\n` +
|
|
60
67
|
` --target=claude|agents|both Where to install (default: both)\n` +
|
|
61
68
|
` --force, -f Overwrite existing skill dir without prompting\n` +
|
|
62
69
|
` --help, -h Show this message\n`,
|
|
@@ -107,7 +114,7 @@ function confirm(question) {
|
|
|
107
114
|
});
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
async function installInto(targetDir, force) {
|
|
117
|
+
async function installInto(skillName, sourceDir, targetDir, force) {
|
|
111
118
|
// Safety: the only thing we ever delete is the existing skill dir at the
|
|
112
119
|
// exact target path. We never touch the parent directory or siblings.
|
|
113
120
|
if (await exists(targetDir)) {
|
|
@@ -120,8 +127,8 @@ async function installInto(targetDir, force) {
|
|
|
120
127
|
}
|
|
121
128
|
await fs.rm(targetDir, { recursive: true, force: true });
|
|
122
129
|
}
|
|
123
|
-
const count = await copyDir(
|
|
124
|
-
console.log(` ✓ Installed ${
|
|
130
|
+
const count = await copyDir(sourceDir, targetDir);
|
|
131
|
+
console.log(` ✓ Installed ${skillName} → ${targetDir} (${count} files)`);
|
|
125
132
|
return count;
|
|
126
133
|
}
|
|
127
134
|
|
|
@@ -143,31 +150,38 @@ async function main() {
|
|
|
143
150
|
process.exit(2);
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
console.error(
|
|
148
|
-
`✖ Source skill not found at ${SOURCE}\n` +
|
|
149
|
-
` This usually means the ${PACKAGE_NAME} package is missing the skill files.\n` +
|
|
150
|
-
` Try reinstalling: npm install ${PACKAGE_NAME}@latest`,
|
|
151
|
-
);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
153
|
+
const skillNames = opts.skill === 'all' ? SKILL_NAMES : [opts.skill];
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
if (opts.target === 'claude' || opts.target === 'both') {
|
|
157
|
-
targets.push(path.join(CWD, '.claude', 'skills', SKILL_NAME));
|
|
158
|
-
}
|
|
159
|
-
if (opts.target === 'agents' || opts.target === 'both') {
|
|
160
|
-
targets.push(path.join(CWD, '.agents', 'skills', SKILL_NAME));
|
|
161
|
-
}
|
|
155
|
+
console.log(`Installing ${skillNames.length === 1 ? `"${skillNames[0]}"` : 'AI skills'} from ${PACKAGE_NAME}\n`);
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
for (const skillName of skillNames) {
|
|
158
|
+
const source = path.join(PACKAGE_ROOT, '.agents', 'skills', skillName);
|
|
159
|
+
if (!(await exists(source))) {
|
|
160
|
+
console.error(
|
|
161
|
+
`✖ Source skill not found at ${source}\n` +
|
|
162
|
+
` This usually means the ${PACKAGE_NAME} package is missing the skill files.\n` +
|
|
163
|
+
` Try reinstalling: npm install ${PACKAGE_NAME}@latest`,
|
|
164
|
+
);
|
|
170
165
|
process.exitCode = 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const targets = [];
|
|
170
|
+
if (opts.target === 'claude' || opts.target === 'both') {
|
|
171
|
+
targets.push(path.join(CWD, '.claude', 'skills', skillName));
|
|
172
|
+
}
|
|
173
|
+
if (opts.target === 'agents' || opts.target === 'both') {
|
|
174
|
+
targets.push(path.join(CWD, '.agents', 'skills', skillName));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const target of targets) {
|
|
178
|
+
try {
|
|
179
|
+
await installInto(skillName, source, target, opts.force);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(` ✖ ${target}`);
|
|
182
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
}
|
|
171
185
|
}
|
|
172
186
|
}
|
|
173
187
|
|