uipkge-ng 0.1.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/LICENSE +22 -0
- package/README.md +371 -0
- package/dist/args.js +7 -0
- package/dist/commands/add.js +216 -0
- package/dist/commands/build.js +121 -0
- package/dist/commands/diff.js +140 -0
- package/dist/commands/info.js +122 -0
- package/dist/commands/init.js +154 -0
- package/dist/commands/list.js +103 -0
- package/dist/commands/mcp.js +149 -0
- package/dist/commands/view.js +48 -0
- package/dist/config.js +56 -0
- package/dist/env.js +33 -0
- package/dist/errors.js +9 -0
- package/dist/extras.js +111 -0
- package/dist/files.js +49 -0
- package/dist/index.js +162 -0
- package/dist/layout.js +114 -0
- package/dist/output.js +26 -0
- package/dist/packages.js +68 -0
- package/dist/project.js +145 -0
- package/dist/prompts.js +27 -0
- package/dist/registry.js +187 -0
- package/dist/resolve.js +60 -0
- package/dist/setup.js +169 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Uday Adaka
|
|
4
|
+
uipkge (https://uipkge.dev) - uipkge-ng, the uipkge CLI for Angular
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# uipkge-ng
|
|
2
|
+
|
|
3
|
+
**Add [uipkge](https://uipkge.dev) components to your Angular app — as source you own.**
|
|
4
|
+
|
|
5
|
+
uipkge-ng is the Angular CLI for the uipkge registry, in the spirit of shadcn/ui: it copies each component's source into your project, installs what it needs and wires up Tailwind. There's no runtime package to depend on and no version to upgrade — the code is yours to read and change.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx uipkge-ng init
|
|
9
|
+
npx uipkge-ng add button dialog date-picker
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { UiButtonComponent } from '@/ui/button';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<button ui-button variant="outline">Save</button>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **One command to set up** — Tailwind CSS v4, the `cn()` helper, design tokens (light and dark) and tsconfig paths.
|
|
21
|
+
- **Dependencies resolved** — adding `date-picker` brings `calendar`, `popover` and their npm packages; `@angular/*` packages are pinned to your Angular version.
|
|
22
|
+
- **Safe in real projects** — it never overwrites your files without asking, works in multi-project workspaces and monorepos, and follows your folder layout.
|
|
23
|
+
- **Your own registries too** — install from private registries with auth, or publish your own with `uipkge-ng build`.
|
|
24
|
+
- **AI-ready** — `uipkge-ng mcp` lets assistants browse components and their source.
|
|
25
|
+
|
|
26
|
+
## Contents
|
|
27
|
+
|
|
28
|
+
- [Requirements](#requirements)
|
|
29
|
+
- [Getting started](#getting-started)
|
|
30
|
+
- [Commands](#commands)
|
|
31
|
+
- [Existing projects](#existing-projects)
|
|
32
|
+
- [Workspaces and monorepos](#workspaces-and-monorepos)
|
|
33
|
+
- [components.json](#componentsjson)
|
|
34
|
+
- [Custom folders](#custom-folders)
|
|
35
|
+
- [Registries](#registries)
|
|
36
|
+
- [Publishing your own registry](#publishing-your-own-registry)
|
|
37
|
+
- [AI assistants (MCP)](#ai-assistants-mcp)
|
|
38
|
+
- [CI and scripting](#ci-and-scripting)
|
|
39
|
+
- [Troubleshooting](#troubleshooting)
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Node.js **20.12** or newer
|
|
44
|
+
- An Angular CLI application with standalone components (Analog/Vite supported; Nx workspaces not yet)
|
|
45
|
+
- npm, pnpm, yarn or bun — detected from your lockfile
|
|
46
|
+
|
|
47
|
+
## Getting started
|
|
48
|
+
|
|
49
|
+
Run it without installing:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx uipkge-ng init # or: pnpm dlx uipkge-ng init / bunx uipkge-ng init
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
or add it to the project and use it from there:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm i -D uipkge-ng
|
|
59
|
+
npx uipkge-ng add button
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`init` shows what it did:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
✔ uipkge is ready.
|
|
66
|
+
✔ Tailwind CSS v4 set up
|
|
67
|
+
✔ tsconfig paths `@/*` and `@/ui/*` added (tsconfig.json)
|
|
68
|
+
✔ cn() helper written (src/lib/utils.ts)
|
|
69
|
+
✔ design tokens written (src/uipkge.css)
|
|
70
|
+
✔ src/styles.css imports ./uipkge.css
|
|
71
|
+
✔ components.json written
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then add components — by name, or run `uipkge-ng add` on its own to pick from a searchable list:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx uipkge-ng add card dialog
|
|
78
|
+
npx uipkge-ng list # everything available, ✔ on what you have
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Dark mode follows a `dark` class on `<html>`.
|
|
82
|
+
|
|
83
|
+
`@uipkge/ng` and `uipkge` are aliases of the same CLI — `npx @uipkge/ng add button` and `npx uipkge add button` work too.
|
|
84
|
+
|
|
85
|
+
## Commands
|
|
86
|
+
|
|
87
|
+
| Command | |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| [`init`](#init) | set up the project |
|
|
90
|
+
| [`add [items...]`](#add) | add components and their dependencies |
|
|
91
|
+
| [`list [@registry] [query]`](#list) | browse components (alias `ls`) |
|
|
92
|
+
| [`view <items...>`](#view) | show a component's details and source |
|
|
93
|
+
| [`diff [item]`](#diff) | compare your copies with the registry |
|
|
94
|
+
| [`info`](#info) | project, config and registry details |
|
|
95
|
+
| [`build [registry.json]`](#publishing-your-own-registry) | build your own registry |
|
|
96
|
+
| [`mcp`](#ai-assistants-mcp) | MCP server for AI assistants |
|
|
97
|
+
|
|
98
|
+
Items can be names (`button`), named-registry items (`@acme/card`), item URLs (`https://…/card.json`) or local item files (`./card.json`).
|
|
99
|
+
|
|
100
|
+
### init
|
|
101
|
+
|
|
102
|
+
Sets the project up once:
|
|
103
|
+
|
|
104
|
+
- installs **Tailwind CSS v4** if it's missing (`tailwindcss`, `@tailwindcss/postcss`, `postcss`) and creates or extends your PostCSS config
|
|
105
|
+
- adds the `@/*` and `@/ui/*` paths to your tsconfig — comments, formatting and existing paths are kept
|
|
106
|
+
- writes the `cn()` helper to `src/lib/utils.ts`
|
|
107
|
+
- writes the design tokens to `src/uipkge.css` and imports them from your global stylesheet (found in `angular.json`). With a Sass/Less stylesheet, `src/uipkge.css` is added to the app's `styles` in `angular.json` instead
|
|
108
|
+
- writes `components.json`
|
|
109
|
+
|
|
110
|
+
Nothing on the network can fail halfway: everything is fetched before the project is touched. Re-running is safe; `--overwrite` rewrites the foundation files.
|
|
111
|
+
|
|
112
|
+
### add
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
uipkge-ng add button dialog
|
|
116
|
+
uipkge-ng add --all
|
|
117
|
+
uipkge-ng add @acme/card
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Adds the components and everything they depend on — other components first, then npm packages. Components you already have are left as they are.
|
|
121
|
+
|
|
122
|
+
| Flag | |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `--dry-run` | list every file that would be created, overwritten or kept, the npm packages and extras — nothing is written |
|
|
125
|
+
| `--view[=<path>]` | print the files that would be written (optionally only those matching `<path>`) |
|
|
126
|
+
| `--diff[=<path>]` | show how they'd differ from your files |
|
|
127
|
+
| `-p, --path <dir>` | put the requested components in `<dir>` (their dependencies stay in the usual folder) |
|
|
128
|
+
| `-o, --overwrite` | replace existing files |
|
|
129
|
+
| `-a, --all` | add every component |
|
|
130
|
+
| `-y, --yes` | don't ask for confirmation |
|
|
131
|
+
|
|
132
|
+
Unknown names stop the command before anything changes. Items can also carry install-time extras, which `add` applies:
|
|
133
|
+
|
|
134
|
+
- `cssVars` / `css` → a marked block in `src/uipkge.css`, replaced (not duplicated) on reinstall; colour variables are registered with Tailwind, so `bg-<name>` works
|
|
135
|
+
- `envVars` → appended to `.env`; existing keys are never touched
|
|
136
|
+
- `docs` → printed after install
|
|
137
|
+
|
|
138
|
+
### list
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
uipkge-ng list # grouped by category
|
|
142
|
+
uipkge-ng list date # search names and descriptions
|
|
143
|
+
uipkge-ng list --category form
|
|
144
|
+
uipkge-ng list --installed
|
|
145
|
+
uipkge-ng list @acme # a named registry
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Works outside a project too. `--json` for scripts.
|
|
149
|
+
|
|
150
|
+
### view
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uipkge-ng view date-picker
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Everything an install would bring — description, dependencies, notes and the full source of every file — without installing. `--json` prints the raw item.
|
|
157
|
+
|
|
158
|
+
### diff
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
uipkge-ng diff # which installed components differ from the registry
|
|
162
|
+
uipkge-ng diff button # the unified diff for one
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Checks what you installed from the default registry and from every named one. Takes any item form: `button`, `@acme/card`, a URL or a local file. Line-ending differences are ignored. To take the registry version, `uipkge-ng add button --overwrite`.
|
|
166
|
+
|
|
167
|
+
### info
|
|
168
|
+
|
|
169
|
+
Prints the CLI and Node versions, the project (app, Angular, package manager, Tailwind, PostCSS), `components.json`, the folders your aliases resolve to, whether the registry is reachable and what's installed. Paste it into bug reports; `--json` for machines.
|
|
170
|
+
|
|
171
|
+
## Existing projects
|
|
172
|
+
|
|
173
|
+
uipkge-ng is built to be run on apps that already have code in them:
|
|
174
|
+
|
|
175
|
+
- **Your files are never overwritten silently.** If a file `add` would write already exists with different content — say your own `button.component.ts` — it stops before writing anything. Interactively it asks; otherwise re-run with `--overwrite`, or install elsewhere with `--path src/app/uipkge`.
|
|
176
|
+
- **Your styles stay yours.** Tokens live in their own file (`src/uipkge.css`); your stylesheet only gains one import line.
|
|
177
|
+
- **Your tsconfig stays yours.** Existing paths are kept. If `@/*` already points somewhere else, uipkge's path is added as a fallback and the helper goes where your mapping resolves.
|
|
178
|
+
- **Your `utils.ts` stays yours.** If `src/lib/utils.ts` exists, `init` keeps it and warns if it doesn't export `cn()`.
|
|
179
|
+
- **Try first.** `add --dry-run` shows exactly what would change, `add --diff` shows how.
|
|
180
|
+
|
|
181
|
+
## Workspaces and monorepos
|
|
182
|
+
|
|
183
|
+
**Angular multi-project workspaces** (`ng new --create-application=false`, apps under `projects/`): run uipkge-ng inside the app's folder, or pass it with `-c`:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npx uipkge-ng init -c projects/admin
|
|
187
|
+
npx uipkge-ng add button -c projects/admin
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Each app gets its own `components.json`, tokens and paths — written to its `tsconfig.app.json` and `tsconfig.spec.json`, keeping the paths it inherits. Packages are installed once, for the workspace. At the workspace root, uipkge-ng asks you to choose rather than guess.
|
|
191
|
+
|
|
192
|
+
**npm, pnpm, yarn and bun workspaces**: run it in the app (`apps/web`). The package manager comes from the workspace's lockfile.
|
|
193
|
+
|
|
194
|
+
**Nx** isn't supported yet.
|
|
195
|
+
|
|
196
|
+
## components.json
|
|
197
|
+
|
|
198
|
+
`init` writes it; you can edit it by hand.
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"framework": "angular",
|
|
203
|
+
"registryUrl": "https://uipkge.dev/r/angular",
|
|
204
|
+
"styles": "src/styles.css",
|
|
205
|
+
"tokens": "src/uipkge.css",
|
|
206
|
+
"aliases": {
|
|
207
|
+
"ui": "@/ui",
|
|
208
|
+
"lib": "@/lib",
|
|
209
|
+
"blocks": "@/app/components/blocks"
|
|
210
|
+
},
|
|
211
|
+
"registries": {}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Field | |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| `registryUrl` | the default registry (the `UIPKGE_REGISTRY_URL` environment variable overrides it) |
|
|
218
|
+
| `styles` | your global stylesheet, which imports the tokens |
|
|
219
|
+
| `tokens` | the file uipkge owns for design tokens and item CSS |
|
|
220
|
+
| `aliases` | where components, helpers and blocks go — see [Custom folders](#custom-folders) |
|
|
221
|
+
| `registries` | named and private registries — see [Registries](#registries) |
|
|
222
|
+
|
|
223
|
+
## Custom folders
|
|
224
|
+
|
|
225
|
+
By default components go to `src/app/components/ui`, helpers to `src/lib` and blocks to `src/app/components/blocks`. For your own layout, point `aliases` at import aliases your tsconfig `paths` map:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{ "aliases": { "ui": "@shared/ui", "lib": "@core/lib", "blocks": "@shared/blocks" } }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```jsonc
|
|
232
|
+
// tsconfig.json
|
|
233
|
+
"paths": { "@shared/*": ["./src/app/shared/*"], "@core/*": ["./src/app/core/*"] }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Every command then uses those folders, and the `@/ui/…` and `@/lib/…` imports inside component sources are rewritten to your aliases. An alias that `paths` doesn't map is an error, so files never land somewhere unexpected. `uipkge-ng info` shows where each alias resolves.
|
|
237
|
+
|
|
238
|
+
## Registries
|
|
239
|
+
|
|
240
|
+
Components come from `https://uipkge.dev/r/angular` unless you configure otherwise. Any shadcn-style registry works — a public one, your company's private one, or one you [build yourself](#publishing-your-own-registry).
|
|
241
|
+
|
|
242
|
+
### Changing the default registry
|
|
243
|
+
|
|
244
|
+
Set `registryUrl` in `components.json`, or `UIPKGE_REGISTRY_URL` in the environment (it wins, which is handy for mirrors and CI):
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{ "registryUrl": "https://registry.acme.dev/r" }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Bare names (`uipkge-ng add card`) then come from that registry. It doesn't need to ship the foundation: if it has no `utils` or `tailwind` item, `init` takes uipkge's and says so.
|
|
251
|
+
|
|
252
|
+
### Named and private registries
|
|
253
|
+
|
|
254
|
+
To use several registries side by side, add them under `registries`; their items are then `@name/item`:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"registries": {
|
|
259
|
+
"@acme": "https://acme.dev/r/{name}.json",
|
|
260
|
+
"@internal": {
|
|
261
|
+
"url": "https://registry.internal.dev/{name}.json",
|
|
262
|
+
"headers": { "Authorization": "Bearer ${INTERNAL_TOKEN}" },
|
|
263
|
+
"params": { "v": "2" }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
uipkge-ng add @acme/card
|
|
271
|
+
uipkge-ng list @internal
|
|
272
|
+
uipkge-ng view @internal/data-table
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- `{name}` becomes the item name; `list` reads the index at `{name}` = `registry`.
|
|
276
|
+
- `${VAR}` in headers comes from the environment, then `.env.local`, then `.env`. A missing variable is an error before any request is made.
|
|
277
|
+
- Headers are only ever sent to that registry's own URLs.
|
|
278
|
+
- `list @name`, `info` and `diff` show which of its items you have. Items added from a URL or a file can be checked with `uipkge-ng diff <url-or-file>`.
|
|
279
|
+
|
|
280
|
+
## Publishing your own registry
|
|
281
|
+
|
|
282
|
+
Describe your items in a `registry.json`, pointing at your source files:
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"name": "acme",
|
|
287
|
+
"homepage": "https://acme.dev",
|
|
288
|
+
"items": [
|
|
289
|
+
{
|
|
290
|
+
"name": "stat-card",
|
|
291
|
+
"type": "registry:ui",
|
|
292
|
+
"description": "A labelled number.",
|
|
293
|
+
"registryDependencies": ["badge"],
|
|
294
|
+
"dependencies": [],
|
|
295
|
+
"files": [
|
|
296
|
+
{
|
|
297
|
+
"path": "src/stat-card/stat-card.ts",
|
|
298
|
+
"type": "registry:ui",
|
|
299
|
+
"target": "~/src/app/components/ui/stat-card/stat-card.ts"
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
npx uipkge-ng build # → public/r/stat-card.json, public/r/registry.json
|
|
309
|
+
npx uipkge-ng build path/to/registry.json --output dist/r
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`build` inlines each file's content into `<item>.json` and writes a `registry.json` index. It checks everything first — names, duplicates, missing files, paths or targets outside the project, dependency formats — reports every problem at once and writes nothing until they all pass.
|
|
313
|
+
|
|
314
|
+
In `registryDependencies`, a bare name (`badge`) is a uipkge component — `build` writes it as its full uipkge URL, so it resolves the same in every project, whatever that project's default registry is. For your own items use `@acme/name` or a full URL; a bare name that matches one of your items is reported as an error. Import `cn` from `@/lib/utils` and other components from `@/ui/…` in your sources — installs rewrite those to each project's aliases. Serve the output folder anywhere static and add it to `registries`.
|
|
315
|
+
|
|
316
|
+
## AI assistants (MCP)
|
|
317
|
+
|
|
318
|
+
`uipkge-ng mcp` runs a read-only [Model Context Protocol](https://modelcontextprotocol.io) server on stdio, so assistants can find components and read their real source instead of guessing APIs:
|
|
319
|
+
|
|
320
|
+
| Tool | |
|
|
321
|
+
| --- | --- |
|
|
322
|
+
| `list_components` | search by query, category or named registry |
|
|
323
|
+
| `view_components` | details, dependencies, notes and full source |
|
|
324
|
+
| `get_add_command` | the `npx uipkge-ng add …` command, checked against the registry |
|
|
325
|
+
| `list_registries` | the default and named registries this project can use |
|
|
326
|
+
|
|
327
|
+
It never writes files — installing stays a command you run. It reads the `components.json` of the directory it starts in, named registries included.
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
// .mcp.json — Claude Code, Cursor and others use the same shape
|
|
331
|
+
{ "mcpServers": { "uipkge": { "command": "npx", "args": ["-y", "uipkge-ng", "mcp"] } } }
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## CI and scripting
|
|
335
|
+
|
|
336
|
+
- Confirmations are skipped automatically when there's no TTY or `CI` is set; `-y` skips them anywhere.
|
|
337
|
+
- Without a TTY, `add` with no names is an error — pass names or `--all`.
|
|
338
|
+
- A file conflict fails with exit code 1 and nothing written; pass `--overwrite` if replacing is intended.
|
|
339
|
+
- `-s, --silent` prints only errors (package-manager output appears only if it fails).
|
|
340
|
+
- `list`, `view` and `info` take `--json`.
|
|
341
|
+
- An option a command doesn't use is an error, not ignored — `init --dry-run` fails rather than initializing.
|
|
342
|
+
- `NO_COLOR` turns colours off.
|
|
343
|
+
|
|
344
|
+
| Option | |
|
|
345
|
+
| --- | --- |
|
|
346
|
+
| `-c, --cwd <dir>` | run in another directory |
|
|
347
|
+
| `-y, --yes` | don't ask for confirmation |
|
|
348
|
+
| `-s, --silent` | only print errors |
|
|
349
|
+
| `--debug` | stack traces on errors |
|
|
350
|
+
| `-h, --help` / `-v, --version` | |
|
|
351
|
+
|
|
352
|
+
## Troubleshooting
|
|
353
|
+
|
|
354
|
+
| Message | Fix |
|
|
355
|
+
| --- | --- |
|
|
356
|
+
| `No components.json found` | run `uipkge-ng init` first |
|
|
357
|
+
| `This workspace has 2 apps: …` | run inside the app's folder, or pass `-c projects/<app>` |
|
|
358
|
+
| `N files already exist with different content` | keep yours and use `--path`, or replace with `--overwrite` |
|
|
359
|
+
| `The "ui" alias "…" is not mapped in tsconfig.json "paths"` | add the mapping, or reset `aliases` to the defaults |
|
|
360
|
+
| `Registry "@acme" needs the environment variable ACME_TOKEN` | set it in your shell, `.env.local` or `.env` |
|
|
361
|
+
| `Nx workspaces are not supported yet` | use a standalone Angular CLI app for now |
|
|
362
|
+
|
|
363
|
+
Still stuck? Include the output of `npx uipkge-ng info` when you [open an issue](https://github.com/uday-a/uipkge-cli/issues).
|
|
364
|
+
|
|
365
|
+
## Credits
|
|
366
|
+
|
|
367
|
+
Created by **Uday Adaka** as part of **[uipkge](https://uipkge.dev)**. It uses the [shadcn/ui](https://ui.shadcn.com) registry format, so any registry in that format can serve Angular items to it.
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
[MIT](./LICENSE) © 2026 Uday Adaka — uipkge. You can use, change and redistribute it freely; keep the copyright and license notice in copies of the CLI.
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--view` / `--diff` take an optional value (`--view` or `--view=src/x.ts`),
|
|
3
|
+
* which node's parseArgs can't express; a bare flag becomes `--flag=` (all files).
|
|
4
|
+
*/
|
|
5
|
+
export function normalizeOptionalValues(argv) {
|
|
6
|
+
return argv.map(arg => (arg === '--view' || arg === '--diff' ? `${arg}=` : arg));
|
|
7
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { registryFor, requireConfig } from '../config.js';
|
|
4
|
+
import { UipkgeError } from '../errors.js';
|
|
5
|
+
import { applyCss, applyEnv, envKeys, hasCss } from '../extras.js';
|
|
6
|
+
import { conflictingFiles, isItemInstalled, targetedFiles, writeItemFiles } from '../files.js';
|
|
7
|
+
import { adaptItem, resolveLayout } from '../layout.js';
|
|
8
|
+
import { color, log, plural } from '../output.js';
|
|
9
|
+
import { installPackages, missingPackages, pinAngular } from '../packages.js';
|
|
10
|
+
import { declaredDependencies, loadProject, resolveTarget } from '../project.js';
|
|
11
|
+
import { confirm, isInteractive, pickComponents } from '../prompts.js';
|
|
12
|
+
import { planInstall } from '../resolve.js';
|
|
13
|
+
import { colorPatch, compareItem } from './diff.js';
|
|
14
|
+
import { firstSentence, isListable } from './list.js';
|
|
15
|
+
export const DEFAULT_UI_DIR = 'src/app/components/ui';
|
|
16
|
+
/**
|
|
17
|
+
* `--path shared/ui` moves an item's files from the UI folder to `shared/ui`,
|
|
18
|
+
* keeping the per-component subfolder. Files outside the UI folder (e.g.
|
|
19
|
+
* `src/lib/…`) stay put. Only requested items move: their dependencies stay in
|
|
20
|
+
* the UI folder, where the `@/ui/*` imports expect them.
|
|
21
|
+
*/
|
|
22
|
+
export function relocate(item, dir, uiDir = DEFAULT_UI_DIR) {
|
|
23
|
+
const base = dir.replace(/\\/g, '/').replace(/^\.?\/+/, '').replace(/\/+$/, '');
|
|
24
|
+
const from = `~/${uiDir}/`;
|
|
25
|
+
return {
|
|
26
|
+
...item,
|
|
27
|
+
files: item.files.map(f => (f.target?.startsWith(from) ? { ...f, target: `~/${base}/${f.target.slice(from.length)}` } : f)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function runAdd(options) {
|
|
31
|
+
const project = await loadProject(options.cwd);
|
|
32
|
+
const { root } = project;
|
|
33
|
+
const config = await requireConfig(root);
|
|
34
|
+
const registry = registryFor(root, config, path.resolve(options.cwd));
|
|
35
|
+
const preview = options.dryRun || options.view !== undefined || options.diff !== undefined;
|
|
36
|
+
const layout = resolveLayout(root, config.aliases);
|
|
37
|
+
let names = [...new Set(options.names)];
|
|
38
|
+
if (options.all || !names.length) {
|
|
39
|
+
const listable = (await registry.index()).items.filter(isListable);
|
|
40
|
+
if (options.all)
|
|
41
|
+
names = listable.map(i => i.name);
|
|
42
|
+
else if (!isInteractive()) {
|
|
43
|
+
throw new UipkgeError('No components given.', 'Pass names (`uipkge-ng add button dialog`) or --all. See `uipkge-ng list`.');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
names = await pickComponents(listable.map(i => ({
|
|
47
|
+
value: i.name,
|
|
48
|
+
label: i.name,
|
|
49
|
+
hint: isItemInstalled(root, adaptItem(i, layout)) ? 'installed' : firstSentence(i.description ?? '').slice(0, 60),
|
|
50
|
+
})));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
log.step('Resolving components');
|
|
54
|
+
const plan = await planInstall(registry, names, item => isItemInstalled(root, item), options.overwrite || preview, (item, requested) => {
|
|
55
|
+
const adapted = adaptItem(item, layout);
|
|
56
|
+
return requested && options.path ? relocate(adapted, options.path, layout.dirs.ui) : adapted;
|
|
57
|
+
});
|
|
58
|
+
const declared = declaredDependencies(project.packageJson);
|
|
59
|
+
const packages = missingPackages(pinAngular(plan.dependencies, project.angularCore), declared);
|
|
60
|
+
const devPackages = missingPackages(pinAngular(plan.devDependencies, project.angularCore), declared);
|
|
61
|
+
// Previews cover what you asked for, plus dependencies that would actually be new.
|
|
62
|
+
const previewPlan = { ...plan, install: plan.install.filter(i => plan.requested.includes(i.name) || !isItemInstalled(root, i)) };
|
|
63
|
+
if (options.view !== undefined)
|
|
64
|
+
return printView(previewPlan, options.view);
|
|
65
|
+
if (options.diff !== undefined)
|
|
66
|
+
return printDiff(root, previewPlan, options.diff);
|
|
67
|
+
if (options.dryRun)
|
|
68
|
+
return printDryRun(root, config.tokens, plan, packages, devPackages, options.overwrite);
|
|
69
|
+
if (!plan.install.length) {
|
|
70
|
+
log.info(`Already installed: ${plan.skipped.join(', ')}. ${color.dim('Use --overwrite to reinstall.')}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Existing files that differ: never write the rest of an item around them.
|
|
74
|
+
let overwrite = options.overwrite;
|
|
75
|
+
const conflicts = overwrite ? [] : plan.install.flatMap(item => conflictingFiles(root, item).map(file => `${file} (${item.name})`));
|
|
76
|
+
if (conflicts.length) {
|
|
77
|
+
const list = conflicts.map(c => ` • ${c}`).join('\n');
|
|
78
|
+
if (options.yes || !isInteractive()) {
|
|
79
|
+
throw new UipkgeError(`${plural(conflicts.length, 'file')} already exist${conflicts.length === 1 ? 's' : ''} with different content:\n${list}`, 'Nothing was changed. Re-run with --overwrite to replace them, or --path <dir> to add the component somewhere else.');
|
|
80
|
+
}
|
|
81
|
+
log.warn(`These files already exist with different content:\n${list}`);
|
|
82
|
+
if (!(await confirm(`Overwrite ${conflicts.length === 1 ? 'it' : 'them'}?`, false))) {
|
|
83
|
+
log.info('Nothing was changed.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
overwrite = true;
|
|
87
|
+
}
|
|
88
|
+
if (!options.yes && isInteractive()) {
|
|
89
|
+
log.info(`${color.bold('Will add')} ${plan.install.map(i => i.name).join(', ')}`);
|
|
90
|
+
if (packages.length || devPackages.length)
|
|
91
|
+
log.info(`${color.bold('npm')} ${[...packages, ...devPackages].join(', ')}`);
|
|
92
|
+
if (!(await confirm('Continue?'))) {
|
|
93
|
+
log.info('Nothing was changed.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (packages.length || devPackages.length) {
|
|
98
|
+
log.step(`Installing ${[...packages, ...devPackages].join(', ')}`);
|
|
99
|
+
await installPackages(project.packageManager, project.packageRoot, packages);
|
|
100
|
+
await installPackages(project.packageManager, project.packageRoot, devPackages, true);
|
|
101
|
+
}
|
|
102
|
+
const kept = [];
|
|
103
|
+
const docs = [];
|
|
104
|
+
const tokensFile = path.join(root, config.tokens);
|
|
105
|
+
for (const item of plan.install) {
|
|
106
|
+
const result = await writeItemFiles(root, item, overwrite);
|
|
107
|
+
kept.push(...result.kept);
|
|
108
|
+
const extras = [];
|
|
109
|
+
if (await applyCss(tokensFile, item))
|
|
110
|
+
extras.push(`css → ${config.tokens}`);
|
|
111
|
+
const envAdded = await applyEnv(path.join(root, '.env'), item);
|
|
112
|
+
if (envAdded.length)
|
|
113
|
+
extras.push(`.env: ${envAdded.join(', ')}`);
|
|
114
|
+
if (item.tailwind)
|
|
115
|
+
log.warn(`${item.name} ships a Tailwind v3 config; Tailwind v4 reads config from CSS, so it was not applied.`);
|
|
116
|
+
if (item.docs)
|
|
117
|
+
docs.push([item.name, item.docs]);
|
|
118
|
+
log.success(`${item.name} ${color.dim(`(${[plural(result.written.length, 'file'), ...extras].join(', ')})`)}`);
|
|
119
|
+
}
|
|
120
|
+
log.blank();
|
|
121
|
+
log.info(`${color.green('✔')} Added ${plural(plan.install.length, 'component')}.`);
|
|
122
|
+
if (plan.skipped.length)
|
|
123
|
+
log.info(color.dim(` Already installed, left as is: ${plan.skipped.join(', ')}`));
|
|
124
|
+
if (kept.length)
|
|
125
|
+
log.info(color.dim(` Existing files kept (use --overwrite to replace): ${kept.join(', ')}`));
|
|
126
|
+
for (const [name, text] of docs) {
|
|
127
|
+
log.blank();
|
|
128
|
+
log.info(`${color.bold(`Notes from ${name}:`)}\n${text.trim()}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function fileStates(root, item, overwrite) {
|
|
132
|
+
return targetedFiles(item).map(f => {
|
|
133
|
+
const abs = resolveTarget(root, f.target);
|
|
134
|
+
const differs = conflictingFiles(root, { ...item, files: [f] }).length > 0;
|
|
135
|
+
return { file: path.relative(root, abs), state: !existsSync(abs) ? 'new' : overwrite ? 'overwrite' : differs ? 'conflict' : 'keep' };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function printDryRun(root, tokens, plan, packages, devPackages, overwrite) {
|
|
139
|
+
const toWrite = plan.install.filter(item => overwrite || !isItemInstalled(root, item));
|
|
140
|
+
const already = plan.install.filter(item => !overwrite && isItemInstalled(root, item)).map(i => i.name);
|
|
141
|
+
log.info(color.bold('Dry run — nothing will be written.'));
|
|
142
|
+
if (!toWrite.length) {
|
|
143
|
+
log.info(`All of it is already installed: ${[...already, ...plan.skipped].join(', ')}.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const label = {
|
|
147
|
+
new: color.green('new '),
|
|
148
|
+
overwrite: color.yellow('overwrite'),
|
|
149
|
+
keep: color.dim('keep '),
|
|
150
|
+
conflict: color.red('conflict '),
|
|
151
|
+
};
|
|
152
|
+
for (const item of toWrite) {
|
|
153
|
+
log.blank();
|
|
154
|
+
log.info(color.bold(item.name));
|
|
155
|
+
for (const { file, state } of fileStates(root, item, overwrite))
|
|
156
|
+
log.info(` ${label[state]} ${file}`);
|
|
157
|
+
if (hasCss(item))
|
|
158
|
+
log.info(` ${color.cyan('css ')} block in ${tokens}`);
|
|
159
|
+
if (item.envVars) {
|
|
160
|
+
const envFile = path.join(root, '.env');
|
|
161
|
+
const existing = existsSync(envFile) ? envKeys(readFileSync(envFile, 'utf8')) : new Set();
|
|
162
|
+
const add = Object.keys(item.envVars).filter(k => !existing.has(k));
|
|
163
|
+
if (add.length)
|
|
164
|
+
log.info(` ${color.cyan('env ')} .env: ${add.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
if (item.docs)
|
|
167
|
+
log.info(` ${color.cyan('notes ')} shown after install`);
|
|
168
|
+
}
|
|
169
|
+
if (already.length)
|
|
170
|
+
log.info(color.dim(`\nAlready installed (skipped): ${already.join(', ')}`));
|
|
171
|
+
if (packages.length)
|
|
172
|
+
log.info(`\n${color.bold('npm install')} ${packages.join(' ')}`);
|
|
173
|
+
if (devPackages.length)
|
|
174
|
+
log.info(`${color.bold('npm install -D')} ${devPackages.join(' ')}`);
|
|
175
|
+
const conflicts = overwrite ? 0 : toWrite.reduce((n, item) => n + conflictingFiles(root, item).length, 0);
|
|
176
|
+
if (conflicts)
|
|
177
|
+
log.warn(`${plural(conflicts, 'file')} differ${conflicts === 1 ? 's' : ''} from yours; add will stop unless you pass --overwrite.`);
|
|
178
|
+
log.info(color.dim('\nRun again without --dry-run to apply.'));
|
|
179
|
+
}
|
|
180
|
+
function matches(file, filter) {
|
|
181
|
+
return !filter || file.replace(/\\/g, '/').includes(filter.replace(/\\/g, '/').replace(/^\.?\//, ''));
|
|
182
|
+
}
|
|
183
|
+
function printView(plan, filter) {
|
|
184
|
+
let shown = 0;
|
|
185
|
+
for (const item of plan.install) {
|
|
186
|
+
for (const f of targetedFiles(item)) {
|
|
187
|
+
const file = f.target.replace(/^~\//, '');
|
|
188
|
+
if (!matches(file, filter))
|
|
189
|
+
continue;
|
|
190
|
+
shown++;
|
|
191
|
+
process.stdout.write(`${color.bold(`── ${file}`)} ${color.dim(`(${item.name})`)}\n${f.content ?? ''}\n`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!shown)
|
|
195
|
+
log.info(filter ? `No file matches "${filter}".` : 'No files.');
|
|
196
|
+
}
|
|
197
|
+
async function printDiff(root, plan, filter) {
|
|
198
|
+
let changed = 0;
|
|
199
|
+
let fresh = 0;
|
|
200
|
+
for (const item of plan.install) {
|
|
201
|
+
for (const status of await compareItem(root, item)) {
|
|
202
|
+
if (!matches(status.file, filter))
|
|
203
|
+
continue;
|
|
204
|
+
if (status.status === 'changed') {
|
|
205
|
+
changed++;
|
|
206
|
+
process.stdout.write(`${colorPatch(status.patch)}\n`);
|
|
207
|
+
}
|
|
208
|
+
else if (status.status === 'missing') {
|
|
209
|
+
fresh++;
|
|
210
|
+
log.info(`${color.green('new file')} ${status.file} ${color.dim(`(${item.name})`)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!changed && !fresh)
|
|
215
|
+
log.success(filter ? `No differences in files matching "${filter}".` : 'No differences — your files match the registry.');
|
|
216
|
+
}
|