ply-ui-cli 1.1.123 → 1.1.125
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/README.md +16 -12
- package/dist/commands/add.js +57 -32
- package/dist/commands/diff.js +34 -16
- package/dist/commands/doctor.js +67 -42
- package/dist/commands/init.js +97 -58
- package/dist/commands/update.js +56 -38
- package/dist/index.js +3 -3
- package/dist/sync.js +18 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ CLI for **Ply** at [ply-ui.com](https://ply-ui.com) — Angular + Tailwind CSS c
|
|
|
6
6
|
♿ **Accessibility (ACR)**: [ply-ui.com/accessibility](https://ply-ui.com/accessibility)
|
|
7
7
|
🖥️ **SSR-safe**: the docs site prerenders every catalog route, enforced by a blocking CI job · [how we verify it](https://ply-ui.com/learn/angular-ssr-safe-component-library/)
|
|
8
8
|
🎨 **Figma Design System**: [Figma Community File](https://www.figma.com/community/file/1662825988518656661)
|
|
9
|
-
✨ **See what you can build for free**: [Live Admin Dashboard Demo](https://
|
|
9
|
+
✨ **See what you can build for free**: [Live Admin Dashboard Demo](https://demo.ply-ui.com/app/dashboard) (Source code: [ply-free-dashboard](https://github.com/ply-ui-ng/ply-free-dashboard))
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx ply-ui-cli init
|
|
@@ -17,14 +17,15 @@ The old npm name `base-ui-cli` is **deprecated** (name collision with [MUI Base
|
|
|
17
17
|
|
|
18
18
|
## What's new
|
|
19
19
|
|
|
20
|
-
- **3-way merge** — `npx ply-ui-cli update --merge` (or the interactive Merge choice) uses `git merge-file` with the install snapshot under `.
|
|
20
|
+
- **3-way merge** — `npx ply-ui-cli update --merge` (or the interactive Merge choice) uses `git merge-file` with the install snapshot under `.ply-ui/snapshots`. Aliases: `sheet` → `drawer`, `carousel` → `slider`.
|
|
21
21
|
- **add picker** — `npx ply-ui-cli add` with no names opens a catalog filter + type-to-select. `--yes` still requires explicit names.
|
|
22
|
-
- **doctor** — `npx ply-ui-cli doctor` checks `
|
|
22
|
+
- **doctor** — `npx ply-ui-cli doctor` checks `ply-ui.json`, Tailwind v4 `@source` paths, `ply-ui.css`, CDK, and icon sprites. Read-only.
|
|
23
23
|
- **hover-card**, **menubar**, **loading-overlay**, **currency-input** (free) — hover preview panel, application menubar, blocking spinner overlay, locale-aware currency field.
|
|
24
24
|
- **kbd**, **aspect-ratio**, **scroll-area**, **cookie-banner** (free) — keyboard glyph, ratio wrapper, themed overflow pane, SSR-safe consent banner.
|
|
25
25
|
- **chat**, **combobox**, **tags-input**, **time-picker** (free) — AI chat kit, searchable combobox, chip tags field, and HH:mm time picker.
|
|
26
26
|
- **progress-button** (free) — button with a stable-width loading spinner: the label fades out, the spinner takes its place, and the button keeps its exact size and color while the async action runs.
|
|
27
27
|
- **shell** (Pro) — unified page/dashboard application shell with optional footer, push drawers, and mini rail.
|
|
28
|
+
- **AI agents** — copy-in install is cheaper in tokens than generating a widget. Walkthrough (simulation, not a lab benchmark): [rich text editor](https://ply-ui.com/learn/ai-agent-tokens-rich-text-editor/).
|
|
28
29
|
|
|
29
30
|
Full history: [ply-ui.com/changelog](https://ply-ui.com/changelog)
|
|
30
31
|
|
|
@@ -56,10 +57,10 @@ outside a server render.
|
|
|
56
57
|
|
|
57
58
|
Sets up your project:
|
|
58
59
|
|
|
59
|
-
- writes `
|
|
60
|
-
- creates `
|
|
60
|
+
- writes `ply-ui.json` (component path aliases),
|
|
61
|
+
- creates `ply-ui.css` (CDK overlay styles, keyframes, autofill fixes) and imports it from your global stylesheet,
|
|
61
62
|
- downloads the icon sprites (`icons.svg`, `icons-filled.svg`) into your assets folder,
|
|
62
|
-
- writes `.cursor/rules/
|
|
63
|
+
- writes `.cursor/rules/ply-ui.mdc` and `.vscode/ply-ui.code-snippets` when those files are missing (Cursor / Windsurf / VS Code). Skip MCP? These files still teach the assistant how to write Ply code.
|
|
63
64
|
|
|
64
65
|
```bash
|
|
65
66
|
npx ply-ui-cli init # interactive
|
|
@@ -119,13 +120,13 @@ npx ply-ui-cli update # interactive — asks about each conflict
|
|
|
119
120
|
npx ply-ui-cli update card # update one component
|
|
120
121
|
npx ply-ui-cli update --yes # non-interactive; conflicts and local-only edits are left untouched
|
|
121
122
|
npx ply-ui-cli update --yes --force # non-interactive; conflicts take the upstream version
|
|
122
|
-
npx ply-ui-cli update --yes --merge # conflicts: 3-way merge using .
|
|
123
|
+
npx ply-ui-cli update --yes --merge # conflicts: 3-way merge using .ply-ui/snapshots
|
|
123
124
|
npx ply-ui-cli update --yes --overwrite-local # also revert files you edited that upstream did not change
|
|
124
125
|
```
|
|
125
126
|
|
|
126
127
|
`--force` only resolves **conflicts** (both sides changed). It does not discard local-only edits. `--merge` needs the snapshot written by `add` (re-add with `--overwrite` if you installed before snapshots existed). Dirty merges leave conflict markers and exit 1.
|
|
127
128
|
|
|
128
|
-
Update tracking uses `
|
|
129
|
+
Update tracking uses `ply-ui-lock.json` (created by `init`), which records a hash of each installed file. Components installed before this existed aren't tracked — re-run `add <name> --overwrite` to start tracking them.
|
|
129
130
|
|
|
130
131
|
### `changelog`
|
|
131
132
|
|
|
@@ -140,7 +141,7 @@ Unknown names get suggestions from the registry. If the item exists but has no m
|
|
|
140
141
|
|
|
141
142
|
### `doctor`
|
|
142
143
|
|
|
143
|
-
Verifies that your project is set up correctly for Ply. It checks `
|
|
144
|
+
Verifies that your project is set up correctly for Ply. It checks `ply-ui.json`, the Angular workspace, `@angular/cdk`, Tailwind v4 and its `@source` paths, the `ply-ui.css` import, the icon sprites, and the components directory. Read-only — nothing is written.
|
|
144
145
|
|
|
145
146
|
```bash
|
|
146
147
|
npx ply-ui-cli doctor
|
|
@@ -163,7 +164,7 @@ Get a license and browse live previews of every component at [ply-ui.com](https:
|
|
|
163
164
|
|
|
164
165
|
## Configuration
|
|
165
166
|
|
|
166
|
-
`
|
|
167
|
+
`ply-ui.json` (created by `init`):
|
|
167
168
|
|
|
168
169
|
```json
|
|
169
170
|
{
|
|
@@ -252,10 +253,13 @@ A one-time Pro license is $99. The Relay workspace, Motif storefront, and Motif
|
|
|
252
253
|
npx ply-ui-cli init once per project, then npx ply-ui-cli add <name>. Omit the name in a terminal to pick from the catalog (filter, then type to select). --yes and CI need explicit names. Components land in src/app/components/<name>/ by default and are yours.
|
|
253
254
|
|
|
254
255
|
**How do I verify my Ply setup?**
|
|
255
|
-
Run npx ply-ui-cli doctor. It checks
|
|
256
|
+
Run npx ply-ui-cli doctor. It checks ply-ui.json, the Angular workspace, @angular/cdk, Tailwind v4 @source paths, the ply-ui.css import, icon sprites, and the components directory — read-only, nothing is written.
|
|
256
257
|
|
|
257
258
|
**Can AI agents install Ply?**
|
|
258
|
-
Yes. npx -y ply-ui-mcp is stdio for Cursor, Claude (Code/Desktop), Kimi, VS Code, Windsurf, and other local hosts. ChatGPT and Gemini chat apps use Streamable HTTP
|
|
259
|
+
Yes. npx -y ply-ui-mcp is stdio for Cursor, Claude (Code/Desktop), Kimi, VS Code, Windsurf, and other local hosts. ChatGPT and Gemini chat apps use the Streamable HTTP connector at https://mcp.ply-ui.com/mcp (catalog tools) or a local npx -y ply-ui-mcp --http without --read-only so add/init/update can see the project. Pro add uses PLY_LICENSE_KEY on the MCP process. Editors that skip MCP can copy the Cursor rule and snippet pack from https://ply-ui.com/getting-started/#editor-rules.
|
|
260
|
+
|
|
261
|
+
**Do AI agents use fewer tokens with Ply?**
|
|
262
|
+
Usually, yes. With Ply the agent copies a finished component (CLI or MCP) and wires it, instead of generating a toolbar, selection model, sanitizer, and SSR guards from a blank file. Token use still depends on the prompt and the model. A reconstructed Claude/Grok session for the rich text editor is at https://ply-ui.com/learn/ai-agent-tokens-rich-text-editor/ — that is a teaching simulation, not a lab benchmark.
|
|
259
263
|
|
|
260
264
|
**What is the risk if Ply disappears?**
|
|
261
265
|
Near zero. Installed code is in your version control with no runtime dependency; deleting the CLI changes nothing about your app.
|
package/dist/commands/add.js
CHANGED
|
@@ -33,30 +33,49 @@ __export(add_exports, {
|
|
|
33
33
|
add: () => add
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(add_exports);
|
|
36
|
-
var
|
|
36
|
+
var import_fs5 = __toESM(require("fs"));
|
|
37
37
|
var import_path5 = __toESM(require("path"));
|
|
38
38
|
var import_vendor2 = require("../vendor");
|
|
39
39
|
var import_registry = require("../registry");
|
|
40
40
|
|
|
41
41
|
// src/lockfile.ts
|
|
42
|
+
var import_fs2 = __toESM(require("fs"));
|
|
43
|
+
var import_crypto = __toESM(require("crypto"));
|
|
44
|
+
|
|
45
|
+
// src/paths.ts
|
|
42
46
|
var import_fs = __toESM(require("fs"));
|
|
43
47
|
var import_path = __toESM(require("path"));
|
|
44
|
-
var
|
|
45
|
-
var
|
|
48
|
+
var CONFIG_FILE = "ply-ui.json";
|
|
49
|
+
var LEGACY_CONFIG_FILE = "base-ui.json";
|
|
50
|
+
var LOCKFILE_NAME = "ply-ui-lock.json";
|
|
51
|
+
var LEGACY_LOCKFILE_NAME = "base-ui-lock.json";
|
|
52
|
+
var SNAPSHOT_ROOT = ".ply-ui/snapshots";
|
|
53
|
+
function firstExisting(cwd, names) {
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
const resolved = import_path.default.resolve(cwd, name);
|
|
56
|
+
if (import_fs.default.existsSync(resolved)) return resolved;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function configFilePath(cwd = process.cwd()) {
|
|
61
|
+
return firstExisting(cwd, [CONFIG_FILE, LEGACY_CONFIG_FILE]);
|
|
62
|
+
}
|
|
63
|
+
function lockfilePath(cwd = process.cwd()) {
|
|
64
|
+
return firstExisting(cwd, [LOCKFILE_NAME, LEGACY_LOCKFILE_NAME]) || import_path.default.resolve(cwd, LOCKFILE_NAME);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/lockfile.ts
|
|
46
68
|
var LOCKFILE_VERSION = 1;
|
|
47
69
|
function hashContent(content) {
|
|
48
70
|
return import_crypto.default.createHash("sha256").update(content, "utf8").digest("hex");
|
|
49
71
|
}
|
|
50
|
-
function lockfilePath() {
|
|
51
|
-
return import_path.default.resolve(process.cwd(), LOCKFILE_NAME);
|
|
52
|
-
}
|
|
53
72
|
function readLockFile() {
|
|
54
73
|
const p = lockfilePath();
|
|
55
|
-
if (!
|
|
74
|
+
if (!import_fs2.default.existsSync(p)) {
|
|
56
75
|
return { version: LOCKFILE_VERSION, components: {} };
|
|
57
76
|
}
|
|
58
77
|
try {
|
|
59
|
-
const parsed = JSON.parse(
|
|
78
|
+
const parsed = JSON.parse(import_fs2.default.readFileSync(p, "utf8"));
|
|
60
79
|
if (parsed && typeof parsed === "object" && parsed.components) {
|
|
61
80
|
return { version: parsed.version ?? LOCKFILE_VERSION, components: parsed.components };
|
|
62
81
|
}
|
|
@@ -65,7 +84,7 @@ function readLockFile() {
|
|
|
65
84
|
return { version: LOCKFILE_VERSION, components: {} };
|
|
66
85
|
}
|
|
67
86
|
function writeLockFile(lock) {
|
|
68
|
-
|
|
87
|
+
import_fs2.default.writeFileSync(lockfilePath(), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
69
88
|
}
|
|
70
89
|
function recordComponent(lock, name, tier, files) {
|
|
71
90
|
const fileHashes = {};
|
|
@@ -80,7 +99,7 @@ function recordComponent(lock, name, tier, files) {
|
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
// src/config.ts
|
|
83
|
-
var
|
|
102
|
+
var import_fs3 = __toESM(require("fs"));
|
|
84
103
|
var import_path2 = __toESM(require("path"));
|
|
85
104
|
|
|
86
105
|
// src/brand.ts
|
|
@@ -91,19 +110,21 @@ var MCP_NPX = `npx -y ${MCP_PACKAGE}`;
|
|
|
91
110
|
|
|
92
111
|
// src/config.ts
|
|
93
112
|
function readConfig() {
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
96
|
-
console.error(
|
|
113
|
+
const resolved = configFilePath();
|
|
114
|
+
if (!resolved) {
|
|
115
|
+
console.error(
|
|
116
|
+
`${CONFIG_FILE} not found (also looked for ${LEGACY_CONFIG_FILE}). Please run "${CLI_NPX} init" first.`
|
|
117
|
+
);
|
|
97
118
|
return null;
|
|
98
119
|
}
|
|
99
120
|
try {
|
|
100
|
-
const config = JSON.parse(
|
|
121
|
+
const config = JSON.parse(import_fs3.default.readFileSync(resolved, "utf8"));
|
|
101
122
|
if (!config || !config.aliases || !config.aliases.components) {
|
|
102
|
-
throw new Error(
|
|
123
|
+
throw new Error(`Missing aliases.components in ${import_path2.default.basename(resolved)}`);
|
|
103
124
|
}
|
|
104
125
|
return config;
|
|
105
126
|
} catch (err) {
|
|
106
|
-
console.error(`Error reading
|
|
127
|
+
console.error(`Error reading ${import_path2.default.basename(resolved)}: ${err.message}`);
|
|
107
128
|
return null;
|
|
108
129
|
}
|
|
109
130
|
}
|
|
@@ -227,7 +248,12 @@ var COMPONENT_ALIASES = {
|
|
|
227
248
|
"image-slider": "slider",
|
|
228
249
|
range: "range-slider",
|
|
229
250
|
"slider-input": "range-slider",
|
|
230
|
-
"input-slider": "range-slider"
|
|
251
|
+
"input-slider": "range-slider",
|
|
252
|
+
"base-list-item": "ply-list-item",
|
|
253
|
+
"base-tab-icon": "ply-tab-icon",
|
|
254
|
+
"base-group-button": "ply-group-button",
|
|
255
|
+
"base-dropdown-menu-item": "ply-dropdown-menu-item",
|
|
256
|
+
"base-context-menu-item": "ply-context-menu-item"
|
|
231
257
|
};
|
|
232
258
|
function canonicalComponentName(name) {
|
|
233
259
|
const key = name.trim().toLowerCase();
|
|
@@ -235,29 +261,28 @@ function canonicalComponentName(name) {
|
|
|
235
261
|
}
|
|
236
262
|
|
|
237
263
|
// src/snapshot.ts
|
|
238
|
-
var
|
|
264
|
+
var import_fs4 = __toESM(require("fs"));
|
|
239
265
|
var import_path4 = __toESM(require("path"));
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return import_path4.default.resolve(process.cwd(), SNAPSHOT_ROOT, component, file);
|
|
266
|
+
function snapshotPath(component, file, root) {
|
|
267
|
+
return import_path4.default.resolve(process.cwd(), root, component, file);
|
|
243
268
|
}
|
|
244
269
|
function writeSnapshot(component, file, content) {
|
|
245
270
|
if (!isSafeFileName(file) || !isSafeFileName(component)) return;
|
|
246
|
-
const dest = snapshotPath(component, file);
|
|
247
|
-
|
|
248
|
-
|
|
271
|
+
const dest = snapshotPath(component, file, SNAPSHOT_ROOT);
|
|
272
|
+
import_fs4.default.mkdirSync(import_path4.default.dirname(dest), { recursive: true });
|
|
273
|
+
import_fs4.default.writeFileSync(dest, content, "utf8");
|
|
249
274
|
}
|
|
250
275
|
|
|
251
276
|
// src/commands/add.ts
|
|
252
277
|
function detectPackageManager(cwd) {
|
|
253
|
-
if (
|
|
254
|
-
if (
|
|
255
|
-
if (
|
|
278
|
+
if (import_fs5.default.existsSync(import_path5.default.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
279
|
+
if (import_fs5.default.existsSync(import_path5.default.join(cwd, "yarn.lock"))) return "yarn";
|
|
280
|
+
if (import_fs5.default.existsSync(import_path5.default.join(cwd, "bun.lockb")) || import_fs5.default.existsSync(import_path5.default.join(cwd, "bun.lock"))) return "bun";
|
|
256
281
|
return "npm";
|
|
257
282
|
}
|
|
258
283
|
function installedNpmDeps(cwd) {
|
|
259
284
|
try {
|
|
260
|
-
const pkg = JSON.parse(
|
|
285
|
+
const pkg = JSON.parse(import_fs5.default.readFileSync(import_path5.default.join(cwd, "package.json"), "utf8"));
|
|
261
286
|
return /* @__PURE__ */ new Set([
|
|
262
287
|
...Object.keys(pkg.dependencies || {}),
|
|
263
288
|
...Object.keys(pkg.devDependencies || {})
|
|
@@ -334,7 +359,7 @@ async function add(components, options = {}) {
|
|
|
334
359
|
return;
|
|
335
360
|
}
|
|
336
361
|
const targetDir = import_path5.default.join(componentsRoot, name);
|
|
337
|
-
if (!requested.has(name) &&
|
|
362
|
+
if (!requested.has(name) && import_fs5.default.existsSync(targetDir)) continue;
|
|
338
363
|
toInstall.set(name, entry);
|
|
339
364
|
queue.push(...entry.registryDependencies);
|
|
340
365
|
}
|
|
@@ -349,7 +374,7 @@ async function add(components, options = {}) {
|
|
|
349
374
|
}
|
|
350
375
|
for (const name of requested) {
|
|
351
376
|
const targetDir = import_path5.default.join(componentsRoot, name);
|
|
352
|
-
if (!
|
|
377
|
+
if (!import_fs5.default.existsSync(targetDir)) continue;
|
|
353
378
|
if (options.overwrite || options.yes) continue;
|
|
354
379
|
const res = await (0, import_vendor2.prompts)({
|
|
355
380
|
type: "confirm",
|
|
@@ -395,8 +420,8 @@ async function add(components, options = {}) {
|
|
|
395
420
|
try {
|
|
396
421
|
for (const file of item.files) {
|
|
397
422
|
const filePath = import_path5.default.join(targetDir, file.name);
|
|
398
|
-
|
|
399
|
-
|
|
423
|
+
import_fs5.default.mkdirSync(import_path5.default.dirname(filePath), { recursive: true });
|
|
424
|
+
import_fs5.default.writeFileSync(filePath, file.content, "utf8");
|
|
400
425
|
writeSnapshot(name, file.name, file.content);
|
|
401
426
|
}
|
|
402
427
|
} catch (err) {
|
package/dist/commands/diff.js
CHANGED
|
@@ -37,8 +37,8 @@ var import_path3 = __toESM(require("path"));
|
|
|
37
37
|
var import_vendor = require("../vendor");
|
|
38
38
|
|
|
39
39
|
// src/config.ts
|
|
40
|
-
var
|
|
41
|
-
var
|
|
40
|
+
var import_fs2 = __toESM(require("fs"));
|
|
41
|
+
var import_path2 = __toESM(require("path"));
|
|
42
42
|
|
|
43
43
|
// src/brand.ts
|
|
44
44
|
var CLI_BIN = "ply-ui-cli";
|
|
@@ -46,40 +46,58 @@ var CLI_NPX = `npx ${CLI_BIN}`;
|
|
|
46
46
|
var MCP_PACKAGE = "ply-ui-mcp";
|
|
47
47
|
var MCP_NPX = `npx -y ${MCP_PACKAGE}`;
|
|
48
48
|
|
|
49
|
+
// src/paths.ts
|
|
50
|
+
var import_fs = __toESM(require("fs"));
|
|
51
|
+
var import_path = __toESM(require("path"));
|
|
52
|
+
var CONFIG_FILE = "ply-ui.json";
|
|
53
|
+
var LEGACY_CONFIG_FILE = "base-ui.json";
|
|
54
|
+
var LOCKFILE_NAME = "ply-ui-lock.json";
|
|
55
|
+
var LEGACY_LOCKFILE_NAME = "base-ui-lock.json";
|
|
56
|
+
function firstExisting(cwd, names) {
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const resolved = import_path.default.resolve(cwd, name);
|
|
59
|
+
if (import_fs.default.existsSync(resolved)) return resolved;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function configFilePath(cwd = process.cwd()) {
|
|
64
|
+
return firstExisting(cwd, [CONFIG_FILE, LEGACY_CONFIG_FILE]);
|
|
65
|
+
}
|
|
66
|
+
function lockfilePath(cwd = process.cwd()) {
|
|
67
|
+
return firstExisting(cwd, [LOCKFILE_NAME, LEGACY_LOCKFILE_NAME]) || import_path.default.resolve(cwd, LOCKFILE_NAME);
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
// src/config.ts
|
|
50
71
|
function readConfig() {
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
console.error(
|
|
72
|
+
const resolved = configFilePath();
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
console.error(
|
|
75
|
+
`${CONFIG_FILE} not found (also looked for ${LEGACY_CONFIG_FILE}). Please run "${CLI_NPX} init" first.`
|
|
76
|
+
);
|
|
54
77
|
return null;
|
|
55
78
|
}
|
|
56
79
|
try {
|
|
57
|
-
const config = JSON.parse(
|
|
80
|
+
const config = JSON.parse(import_fs2.default.readFileSync(resolved, "utf8"));
|
|
58
81
|
if (!config || !config.aliases || !config.aliases.components) {
|
|
59
|
-
throw new Error(
|
|
82
|
+
throw new Error(`Missing aliases.components in ${import_path2.default.basename(resolved)}`);
|
|
60
83
|
}
|
|
61
84
|
return config;
|
|
62
85
|
} catch (err) {
|
|
63
|
-
console.error(`Error reading
|
|
86
|
+
console.error(`Error reading ${import_path2.default.basename(resolved)}: ${err.message}`);
|
|
64
87
|
return null;
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
|
|
68
91
|
// src/lockfile.ts
|
|
69
|
-
var
|
|
70
|
-
var import_path2 = __toESM(require("path"));
|
|
71
|
-
var LOCKFILE_NAME = "base-ui-lock.json";
|
|
92
|
+
var import_fs3 = __toESM(require("fs"));
|
|
72
93
|
var LOCKFILE_VERSION = 1;
|
|
73
|
-
function lockfilePath() {
|
|
74
|
-
return import_path2.default.resolve(process.cwd(), LOCKFILE_NAME);
|
|
75
|
-
}
|
|
76
94
|
function readLockFile() {
|
|
77
95
|
const p = lockfilePath();
|
|
78
|
-
if (!
|
|
96
|
+
if (!import_fs3.default.existsSync(p)) {
|
|
79
97
|
return { version: LOCKFILE_VERSION, components: {} };
|
|
80
98
|
}
|
|
81
99
|
try {
|
|
82
|
-
const parsed = JSON.parse(
|
|
100
|
+
const parsed = JSON.parse(import_fs3.default.readFileSync(p, "utf8"));
|
|
83
101
|
if (parsed && typeof parsed === "object" && parsed.components) {
|
|
84
102
|
return { version: parsed.version ?? LOCKFILE_VERSION, components: parsed.components };
|
|
85
103
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -33,8 +33,8 @@ __export(doctor_exports, {
|
|
|
33
33
|
doctor: () => doctor
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(doctor_exports);
|
|
36
|
-
var
|
|
37
|
-
var
|
|
36
|
+
var import_fs2 = __toESM(require("fs"));
|
|
37
|
+
var import_path2 = __toESM(require("path"));
|
|
38
38
|
var import_vendor = require("../vendor");
|
|
39
39
|
var import_registry = require("../registry");
|
|
40
40
|
|
|
@@ -44,6 +44,27 @@ var CLI_NPX = `npx ${CLI_BIN}`;
|
|
|
44
44
|
var MCP_PACKAGE = "ply-ui-mcp";
|
|
45
45
|
var MCP_NPX = `npx -y ${MCP_PACKAGE}`;
|
|
46
46
|
|
|
47
|
+
// src/paths.ts
|
|
48
|
+
var import_fs = __toESM(require("fs"));
|
|
49
|
+
var import_path = __toESM(require("path"));
|
|
50
|
+
var CONFIG_FILE = "ply-ui.json";
|
|
51
|
+
var LEGACY_CONFIG_FILE = "base-ui.json";
|
|
52
|
+
var CSS_FILE = "ply-ui.css";
|
|
53
|
+
var LEGACY_CSS_FILE = "base-ui.css";
|
|
54
|
+
function firstExisting(cwd, names) {
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
const resolved = import_path.default.resolve(cwd, name);
|
|
57
|
+
if (import_fs.default.existsSync(resolved)) return resolved;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function configFilePath(cwd = process.cwd()) {
|
|
62
|
+
return firstExisting(cwd, [CONFIG_FILE, LEGACY_CONFIG_FILE]);
|
|
63
|
+
}
|
|
64
|
+
function cssImportCandidates() {
|
|
65
|
+
return [`@import './${CSS_FILE}'`, `@import './${LEGACY_CSS_FILE}'`];
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
// src/commands/doctor.ts
|
|
48
69
|
var ICON_SPRITES = ["icons.svg", "icons-filled.svg"];
|
|
49
70
|
function pickAngularProject(angularJson) {
|
|
@@ -56,7 +77,7 @@ function pickAngularProject(angularJson) {
|
|
|
56
77
|
function resolveAssetsDir(cwd, angularJson) {
|
|
57
78
|
const picked = pickAngularProject(angularJson);
|
|
58
79
|
const buildOptions = picked?.project?.architect?.build?.options;
|
|
59
|
-
if (!buildOptions) return
|
|
80
|
+
if (!buildOptions) return import_path2.default.resolve(cwd, "src/assets");
|
|
60
81
|
const assets = buildOptions.assets || [];
|
|
61
82
|
const hasSrcAssets = assets.some(
|
|
62
83
|
(a) => a === "src/assets" || typeof a === "object" && a?.input === "src/assets"
|
|
@@ -64,68 +85,68 @@ function resolveAssetsDir(cwd, angularJson) {
|
|
|
64
85
|
const publicEntry = assets.find(
|
|
65
86
|
(a) => a === "public" || typeof a === "object" && a?.input === "public"
|
|
66
87
|
);
|
|
67
|
-
if (hasSrcAssets) return
|
|
68
|
-
if (publicEntry) return
|
|
69
|
-
return
|
|
88
|
+
if (hasSrcAssets) return import_path2.default.resolve(cwd, "src/assets");
|
|
89
|
+
if (publicEntry) return import_path2.default.resolve(cwd, "public/assets");
|
|
90
|
+
return import_path2.default.resolve(cwd, "src/assets");
|
|
70
91
|
}
|
|
71
92
|
function readJsonSafe(filePath) {
|
|
72
93
|
try {
|
|
73
|
-
return JSON.parse(
|
|
94
|
+
return JSON.parse(import_fs2.default.readFileSync(filePath, "utf8"));
|
|
74
95
|
} catch {
|
|
75
96
|
return null;
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
function readTextSafe(filePath) {
|
|
79
100
|
try {
|
|
80
|
-
return
|
|
101
|
+
return import_fs2.default.readFileSync(filePath, "utf8");
|
|
81
102
|
} catch {
|
|
82
103
|
return null;
|
|
83
104
|
}
|
|
84
105
|
}
|
|
85
106
|
function sourceCovers(source, tailwindDir, targetDir) {
|
|
86
107
|
const raw = source.replace(/[*?[\]{}].*$/, "");
|
|
87
|
-
const resolved =
|
|
88
|
-
const resolvedDir = raw.endsWith("/") || raw.endsWith("\\") ||
|
|
89
|
-
return targetDir === resolvedDir || targetDir.startsWith(resolvedDir +
|
|
108
|
+
const resolved = import_path2.default.resolve(tailwindDir, raw);
|
|
109
|
+
const resolvedDir = raw.endsWith("/") || raw.endsWith("\\") || import_path2.default.extname(raw) === "" ? resolved : import_path2.default.dirname(resolved);
|
|
110
|
+
return targetDir === resolvedDir || targetDir.startsWith(resolvedDir + import_path2.default.sep);
|
|
90
111
|
}
|
|
91
112
|
async function doctor(options = {}) {
|
|
92
113
|
const cwd = options.cwd || process.cwd();
|
|
93
|
-
const relative = (filePath) =>
|
|
114
|
+
const relative = (filePath) => import_path2.default.relative(cwd, filePath) || ".";
|
|
94
115
|
const spinner = (0, import_vendor.ora)("Checking Ply setup...").start();
|
|
95
116
|
const checks = [];
|
|
96
|
-
const configPath =
|
|
117
|
+
const configPath = configFilePath(cwd);
|
|
97
118
|
let config = null;
|
|
98
|
-
if (!
|
|
119
|
+
if (!configPath) {
|
|
99
120
|
checks.push({
|
|
100
|
-
name:
|
|
121
|
+
name: CONFIG_FILE,
|
|
101
122
|
status: "error",
|
|
102
|
-
message: `Not found. Run "${CLI_NPX} init" first.`
|
|
123
|
+
message: `Not found (also looked for ${LEGACY_CONFIG_FILE}). Run "${CLI_NPX} init" first.`
|
|
103
124
|
});
|
|
104
125
|
} else {
|
|
105
126
|
config = readJsonSafe(configPath);
|
|
106
127
|
if (!config) {
|
|
107
128
|
checks.push({
|
|
108
|
-
name:
|
|
129
|
+
name: CONFIG_FILE,
|
|
109
130
|
status: "error",
|
|
110
131
|
message: `${relative(configPath)} exists but is not valid JSON.`
|
|
111
132
|
});
|
|
112
133
|
} else if (!config.aliases?.components) {
|
|
113
134
|
checks.push({
|
|
114
|
-
name:
|
|
135
|
+
name: CONFIG_FILE,
|
|
115
136
|
status: "error",
|
|
116
137
|
message: `${relative(configPath)} is missing aliases.components.`
|
|
117
138
|
});
|
|
118
139
|
} else {
|
|
119
140
|
checks.push({
|
|
120
|
-
name:
|
|
141
|
+
name: CONFIG_FILE,
|
|
121
142
|
status: "ok",
|
|
122
143
|
message: `${relative(configPath)} exists and aliases.components is set.`
|
|
123
144
|
});
|
|
124
145
|
}
|
|
125
146
|
}
|
|
126
|
-
const angularJsonPath =
|
|
147
|
+
const angularJsonPath = import_path2.default.resolve(cwd, "angular.json");
|
|
127
148
|
let angularJson = null;
|
|
128
|
-
if (!
|
|
149
|
+
if (!import_fs2.default.existsSync(angularJsonPath)) {
|
|
129
150
|
checks.push({
|
|
130
151
|
name: "Angular workspace",
|
|
131
152
|
status: "warn",
|
|
@@ -148,7 +169,7 @@ async function doctor(options = {}) {
|
|
|
148
169
|
});
|
|
149
170
|
}
|
|
150
171
|
}
|
|
151
|
-
const packageJsonPath =
|
|
172
|
+
const packageJsonPath = import_path2.default.resolve(cwd, "package.json");
|
|
152
173
|
const packageJson = readJsonSafe(packageJsonPath);
|
|
153
174
|
if (!packageJson) {
|
|
154
175
|
checks.push({
|
|
@@ -175,13 +196,13 @@ async function doctor(options = {}) {
|
|
|
175
196
|
});
|
|
176
197
|
}
|
|
177
198
|
}
|
|
178
|
-
const tailwindPath =
|
|
199
|
+
const tailwindPath = import_path2.default.resolve(cwd, config?.tailwind?.config || "src/tailwind.css");
|
|
179
200
|
const tailwindContent = readTextSafe(tailwindPath);
|
|
180
201
|
if (!tailwindContent) {
|
|
181
202
|
checks.push({
|
|
182
203
|
name: "Tailwind entry",
|
|
183
204
|
status: "error",
|
|
184
|
-
message: `${relative(tailwindPath)} not found. Set tailwind.config in
|
|
205
|
+
message: `${relative(tailwindPath)} not found. Set tailwind.config in ${CONFIG_FILE} or create it.`
|
|
185
206
|
});
|
|
186
207
|
} else {
|
|
187
208
|
checks.push({
|
|
@@ -211,8 +232,8 @@ async function doctor(options = {}) {
|
|
|
211
232
|
});
|
|
212
233
|
} else {
|
|
213
234
|
const componentsAlias2 = config?.aliases?.components || "src/app/components";
|
|
214
|
-
const componentsDir =
|
|
215
|
-
const tailwindDir =
|
|
235
|
+
const componentsDir = import_path2.default.resolve(cwd, componentsAlias2);
|
|
236
|
+
const tailwindDir = import_path2.default.dirname(tailwindPath);
|
|
216
237
|
const hasComponentSource = sourceMatches.some((m) => {
|
|
217
238
|
const raw = m.match(/@source\s+["']([^"']+)["']/)?.[1];
|
|
218
239
|
if (!raw) return false;
|
|
@@ -233,46 +254,50 @@ async function doctor(options = {}) {
|
|
|
233
254
|
}
|
|
234
255
|
}
|
|
235
256
|
}
|
|
236
|
-
const stylesPath =
|
|
257
|
+
const stylesPath = import_path2.default.resolve(cwd, config?.tailwind?.css || "src/styles.scss");
|
|
237
258
|
const stylesContent = readTextSafe(stylesPath);
|
|
238
259
|
if (!stylesContent) {
|
|
239
260
|
checks.push({
|
|
240
261
|
name: "Global styles",
|
|
241
262
|
status: "error",
|
|
242
|
-
message: `${relative(stylesPath)} not found. Set tailwind.css in
|
|
263
|
+
message: `${relative(stylesPath)} not found. Set tailwind.css in ${CONFIG_FILE} or create it.`
|
|
243
264
|
});
|
|
244
265
|
} else {
|
|
245
|
-
const
|
|
246
|
-
|
|
266
|
+
const cssImports = cssImportCandidates();
|
|
267
|
+
const hasCssImport = cssImports.some((line) => stylesContent.includes(line));
|
|
268
|
+
if (!hasCssImport) {
|
|
247
269
|
checks.push({
|
|
248
270
|
name: "Global styles",
|
|
249
271
|
status: "warn",
|
|
250
|
-
message: `${relative(stylesPath)} does not import
|
|
272
|
+
message: `${relative(stylesPath)} does not import './${CSS_FILE}'. Ply global styles will be missing.`
|
|
251
273
|
});
|
|
252
274
|
} else {
|
|
253
275
|
checks.push({
|
|
254
276
|
name: "Global styles",
|
|
255
277
|
status: "ok",
|
|
256
|
-
message: `${relative(stylesPath)} imports
|
|
278
|
+
message: `${relative(stylesPath)} imports Ply global styles.`
|
|
257
279
|
});
|
|
258
280
|
}
|
|
259
|
-
const
|
|
260
|
-
|
|
281
|
+
const stylesDir = import_path2.default.dirname(stylesPath);
|
|
282
|
+
const cssPath = import_path2.default.join(stylesDir, CSS_FILE);
|
|
283
|
+
const legacyCssPath = import_path2.default.join(stylesDir, LEGACY_CSS_FILE);
|
|
284
|
+
const resolvedCss = import_fs2.default.existsSync(cssPath) ? cssPath : import_fs2.default.existsSync(legacyCssPath) ? legacyCssPath : null;
|
|
285
|
+
if (!resolvedCss) {
|
|
261
286
|
checks.push({
|
|
262
|
-
name:
|
|
287
|
+
name: CSS_FILE,
|
|
263
288
|
status: "error",
|
|
264
|
-
message: `${relative(
|
|
289
|
+
message: `${relative(cssPath)} not found. Run "${CLI_NPX} init" to create it.`
|
|
265
290
|
});
|
|
266
291
|
} else {
|
|
267
292
|
checks.push({
|
|
268
|
-
name:
|
|
293
|
+
name: CSS_FILE,
|
|
269
294
|
status: "ok",
|
|
270
|
-
message: `${relative(
|
|
295
|
+
message: `${relative(resolvedCss)} exists.`
|
|
271
296
|
});
|
|
272
297
|
}
|
|
273
298
|
}
|
|
274
|
-
const assetsDir = angularJson ? resolveAssetsDir(cwd, angularJson) :
|
|
275
|
-
const missingSprites = ICON_SPRITES.filter((s) => !
|
|
299
|
+
const assetsDir = angularJson ? resolveAssetsDir(cwd, angularJson) : import_path2.default.resolve(cwd, "src/assets");
|
|
300
|
+
const missingSprites = ICON_SPRITES.filter((s) => !import_fs2.default.existsSync(import_path2.default.join(assetsDir, s)));
|
|
276
301
|
if (missingSprites.length) {
|
|
277
302
|
checks.push({
|
|
278
303
|
name: "Icon sprites",
|
|
@@ -288,8 +313,8 @@ async function doctor(options = {}) {
|
|
|
288
313
|
}
|
|
289
314
|
const componentsAlias = config?.aliases?.components;
|
|
290
315
|
if (componentsAlias) {
|
|
291
|
-
const componentsDir =
|
|
292
|
-
if (!
|
|
316
|
+
const componentsDir = import_path2.default.resolve(cwd, componentsAlias);
|
|
317
|
+
if (!import_fs2.default.existsSync(componentsDir)) {
|
|
293
318
|
checks.push({
|
|
294
319
|
name: "Components directory",
|
|
295
320
|
status: "warn",
|
package/dist/commands/init.js
CHANGED
|
@@ -33,26 +33,46 @@ __export(init_exports, {
|
|
|
33
33
|
init: () => init
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(init_exports);
|
|
36
|
-
var
|
|
36
|
+
var import_fs3 = __toESM(require("fs"));
|
|
37
37
|
var import_path2 = __toESM(require("path"));
|
|
38
38
|
var import_vendor = require("../vendor");
|
|
39
39
|
var import_registry = require("../registry");
|
|
40
40
|
|
|
41
41
|
// src/lockfile.ts
|
|
42
|
+
var import_fs2 = __toESM(require("fs"));
|
|
43
|
+
|
|
44
|
+
// src/paths.ts
|
|
42
45
|
var import_fs = __toESM(require("fs"));
|
|
43
46
|
var import_path = __toESM(require("path"));
|
|
44
|
-
var
|
|
45
|
-
var
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
var CONFIG_FILE = "ply-ui.json";
|
|
48
|
+
var CSS_FILE = "ply-ui.css";
|
|
49
|
+
var LEGACY_CSS_FILE = "base-ui.css";
|
|
50
|
+
var LOCKFILE_NAME = "ply-ui-lock.json";
|
|
51
|
+
var LEGACY_LOCKFILE_NAME = "base-ui-lock.json";
|
|
52
|
+
var CURSOR_RULE_REL = ".cursor/rules/ply-ui.mdc";
|
|
53
|
+
var SNIPPETS_REL = ".vscode/ply-ui.code-snippets";
|
|
54
|
+
var DOCS_CURSOR_RULE = "docs/ai/ply-ui.mdc";
|
|
55
|
+
var DOCS_SNIPPETS = "docs/ai/ply-ui.code-snippets";
|
|
56
|
+
function firstExisting(cwd, names) {
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const resolved = import_path.default.resolve(cwd, name);
|
|
59
|
+
if (import_fs.default.existsSync(resolved)) return resolved;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
48
62
|
}
|
|
63
|
+
function lockfilePath(cwd = process.cwd()) {
|
|
64
|
+
return firstExisting(cwd, [LOCKFILE_NAME, LEGACY_LOCKFILE_NAME]) || import_path.default.resolve(cwd, LOCKFILE_NAME);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/lockfile.ts
|
|
68
|
+
var LOCKFILE_VERSION = 1;
|
|
49
69
|
function readLockFile() {
|
|
50
70
|
const p = lockfilePath();
|
|
51
|
-
if (!
|
|
71
|
+
if (!import_fs2.default.existsSync(p)) {
|
|
52
72
|
return { version: LOCKFILE_VERSION, components: {} };
|
|
53
73
|
}
|
|
54
74
|
try {
|
|
55
|
-
const parsed = JSON.parse(
|
|
75
|
+
const parsed = JSON.parse(import_fs2.default.readFileSync(p, "utf8"));
|
|
56
76
|
if (parsed && typeof parsed === "object" && parsed.components) {
|
|
57
77
|
return { version: parsed.version ?? LOCKFILE_VERSION, components: parsed.components };
|
|
58
78
|
}
|
|
@@ -61,11 +81,11 @@ function readLockFile() {
|
|
|
61
81
|
return { version: LOCKFILE_VERSION, components: {} };
|
|
62
82
|
}
|
|
63
83
|
function writeLockFile(lock) {
|
|
64
|
-
|
|
84
|
+
import_fs2.default.writeFileSync(lockfilePath(), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
// src/templates/editor-kit.generated.ts
|
|
68
|
-
var CURSOR_RULE = '---\ndescription: Ply \u2014 standalone, zoneless, Tailwind v4 copy-in components\nalwaysApply: true\n---\n\n# Ply coding rules\n\nYou are generating Angular for a project that uses **Ply** at https://ply-ui.com \u2014 copy-in components, not an npm UI kit. Not MUI Base UI (React).\n\n## Install, don\'t invent\n\n- Install with `npx ply-ui-cli add <name> --yes`. Source lands in `src/app/components/<name>/`. Humans can run `npx ply-ui-cli add` with no name to pick from the catalog. Never use `add --yes` without a name.\n- Import from that local path. Never `from \'base-ui\'` / `@base-ui/*`.\n- Prefer an existing registry item over a custom control. Check `npx ply-ui-cli list`, https://ply-ui.com/llms.txt, or MCP (`npx -y ply-ui-mcp`) before building a dialog, table, combobox, or form control.\n- If components look unstyled or icons are missing, run `npx ply-ui-cli doctor` (read-only). It checks `
|
|
88
|
+
var CURSOR_RULE = '---\ndescription: Ply \u2014 standalone, zoneless, Tailwind v4 copy-in components\nalwaysApply: true\n---\n\n# Ply coding rules\n\nYou are generating Angular for a project that uses **Ply** at https://ply-ui.com \u2014 copy-in components, not an npm UI kit. Not MUI Base UI (React).\n\n## Install, don\'t invent\n\n- Install with `npx ply-ui-cli add <name> --yes`. Source lands in `src/app/components/<name>/`. Humans can run `npx ply-ui-cli add` with no name to pick from the catalog. Never use `add --yes` without a name.\n- Import from that local path. Never `from \'base-ui\'` / `@base-ui/*`.\n- Prefer an existing registry item over a custom control. Check `npx ply-ui-cli list`, https://ply-ui.com/llms.txt, or MCP (`npx -y ply-ui-mcp`) before building a dialog, table, combobox, or form control.\n- If components look unstyled or icons are missing, run `npx ply-ui-cli doctor` (read-only). It checks `ply-ui.json`, Angular CDK, Tailwind v4 `@source` paths, `ply-ui.css`, and icon sprites.\n- Free items need no license. Pro items need `PLY_LICENSE_KEY` \u2014 do not scrape keys or paste Pro source from memory.\n\n## Angular 22 shape\n\n- New components: `standalone: true`, `changeDetection: ChangeDetectionStrategy.OnPush`.\n- Public APIs: `input()`, `output()`, `model()` \u2014 not `@Input()` / `@Output()`.\n- Templates: `@if` / `@for` / `@empty` \u2014 not `*ngIf` / `*ngFor`.\n- Prefer zoneless. Do not add `zone.js`. After async work, write to signals (or signal-backed CVA) so the view updates without Zone.\n- App selectors stay `app-`. Only Ply selectors use the `ply-` prefix.\n\n## Selectors and forms\n\n- Components: `<ply-card>`, `<ply-dialog>` is not a tag \u2014 open modals with `inject(DialogService).open(MyComponent)`.\n- Directives: `[ply-button]`, `[ply-stroked-button]`, `[ply-input]`, `[ply-textarea]`.\n- Native fields: `<input ply-input [(ngModel)]="\u2026">` or Angular 22 `[formField]` + `ply-input`.\n- `data-table` cells: project `<ng-template plyTableCell="columnKey" let-row let-value="value">`. Combobox options: `plyComboboxOption`. Custom select: `plySelectOption`. Do not fork those components to render a badge or a row action.\n- Chrome strings (empty states, Close, paginator): `provideBaseUiI18n({ close: \'Fermer\' })` in `app.config.ts`. Per-instance `emptyMessage` / `emptyText` still override. Not ngx-translate.\n- Icon sprites: `provideBaseUI({ iconPath: \'assets/icons.svg\' })` in `app.config.ts`. Per-instance `[path]` / `[filledPath]` / `[size]` still win. `w-*`/`h-*` classes keep class-based sizing.\n- Tests: use the copied `*.harness.ts` for data-table, custom-select, and dialog. `TestbedHarnessEnvironment.loader(fixture)`; dialogs need `documentRootLoader` (overlay on `document.body`). Do not invent query helpers for those three.\n- CVA hosts (`ply-combobox`, `ply-tags-input`, `ply-toggle`, `ply-custom-select`, `ply-file-upload`, `ply-mention-input`, `ply-password-input`) bind `[(ngModel)]` / `[formField]` on the host, not on an inner native input.\n- Do not nest composite controls inside `ply-input-group` \u2014 only `[ply-input]`, `[ply-textarea]`, and `ply-mention-input` are projected. Use `ply-password-input` for password fields (it already includes a group).\n- Confirm destructive actions with `inject(DialogService).confirm({ title, destructive: true })` \u2014 do not copy a one-off confirm dialog.\n\n## Tailwind v4\n\n- Style with utilities. Pair light and `dark:` variants (`bg-white dark:bg-slate-800`). Dark mode is the `dark` class on `<html>`.\n- Prefer pixel arbitrary values in new UI (`p-[16px]`, `gap-[8px]`) so spacing does not depend on a custom theme.\n- `color="primary"` reads `--ply-primary` from `ply-ui.css`. Theme Studio Copy CSS writes the same tokens. Unmigrated widgets still follow `--color-blue-*`.\n- Button and input `class` values merge via `cn()`. `class="rounded-full"` and `class="absolute \u2026"` override defaults. Existing `utility!` still works.\n- `<ply-icon>` outlines: `stroke-*`. Filled icons: `[filled]="true"` and `text-*` / `fill-*`.\n\n## Async, overlays, SSR\n\n- `DialogService.open()` is a no-op during SSR. `DialogService.confirm()` emits `false` on the server. Toast and dialog hosts append via `DOCUMENT`.\n- Drawers need `provideAnimationsAsync()`.\n- Close popovers/drawers yourself on in-panel navigation (`popover.close()`, `drawer.closed.emit()`).\n- Overlay menus (dropdown, context) use `a11y-keyboard` (roving tabindex, Home/End, typeahead). Combobox, custom-select, and command-palette use `aria-activedescendant` \u2014 do not switch those to the menu helper.\n- Do not put `ply-toast-container` in templates; the toast service owns the overlay.\n\n## Agents\n\n- MCP optional: `.cursor/mcp.json` \u2192 `npx -y ply-ui-mcp` with `PLY_CWD` = this app root.\n- Facts: https://ply-ui.com/llms.txt \xB7 catalog: https://ply-ui.com/docs/ai/components.md\n- Usage snippets (VS Code / Cursor): `.vscode/ply-ui.code-snippets` \u2014 expand prefixes like `ply-card` after the CLI has copied the component in.\n';
|
|
69
89
|
var CODE_SNIPPETS = '{\n "Ply table": {\n "prefix": "ply-table",\n "scope": "html",\n "body": [\n "<div class=\\"relative w-full overflow-auto\\">",\n " <table ply-table>",\n " <caption ply-table-caption>${1:A list of recent invoices.}</caption>",\n " <thead ply-table-header>",\n " <tr ply-table-row>",\n " <th ply-table-head>${2:Invoice}</th>",\n " <th ply-table-head>${3:Status}</th>",\n " </tr>",\n " </thead>",\n " <tbody ply-table-body>",\n " <tr ply-table-row>",\n " <td ply-table-cell>$0</td>",\n " <td ply-table-cell></td>",\n " </tr>",\n " </tbody>",\n " </table>",\n "</div>"\n ],\n "description": "Ply free HTML table. Install: npx ply-ui-cli add table --yes"\n },\n "Ply card": {\n "prefix": "ply-card",\n "scope": "html",\n "body": [\n "<ply-card>",\n " <ply-card-header class=\\"flex items-center justify-between gap-[8px]\\">",\n " <h2 class=\\"m-0! text-base font-semibold text-slate-900 dark:text-white\\">${1:Title}</h2>",\n " </ply-card-header>",\n " <ply-card-body class=\\"p-[16px]\\">",\n " $0",\n " </ply-card-body>",\n "</ply-card>"\n ],\n "description": "Ply card usage. Install: npx ply-ui-cli add card --yes"\n },\n "Ply button": {\n "prefix": "ply-button",\n "scope": "html",\n "body": [\n "<button type=\\"button\\" ply-button color=\\"${1:primary}\\">${2:Save}</button>"\n ],\n "description": "Ply filled button directive. Install: npx ply-ui-cli add button --yes"\n },\n "Ply stroked button": {\n "prefix": "ply-stroked-button",\n "scope": "html",\n "body": [\n "<button type=\\"button\\" ply-stroked-button color=\\"${1:default}\\">${2:Cancel}</button>"\n ],\n "description": "Ply stroked button directive. Install: npx ply-ui-cli add button --yes"\n },\n "Ply dialog open": {\n "prefix": "ply-dialog",\n "scope": "typescript",\n "body": [\n "this.dialog.open(${1:MyDialogComponent}).subscribe((result) => {",\n " if (!result) return;",\n " $0",\n "});"\n ],\n "description": "Open a Base UI dialog. Install: npx ply-ui-cli add dialog --yes"\n },\n "Ply dialog confirm": {\n "prefix": "ply-confirm",\n "scope": "typescript",\n "body": [\n "this.dialog.confirm({",\n " title: \'${1:Delete this item?}\',",\n " description: \'${2:This cannot be undone.}\',",\n " destructive: true",\n "}).subscribe((ok) => {",\n " if (!ok) return;",\n " $0",\n "});"\n ],\n "description": "Confirm dialog. Install: npx ply-ui-cli add dialog --yes"\n },\n "Ply password input": {\n "prefix": "ply-password",\n "scope": "html",\n "body": [\n "<ply-password-input label=\\"${1:Password}\\" [(ngModel)]=\\"${2:password}\\" [showStrength]=\\"true\\">",\n " <ply-error>${3:Password is required}</ply-error>",\n "</ply-password-input>"\n ],\n "description": "Password field with show/hide. Install: npx ply-ui-cli add password-input --yes"\n },\n "Ply input group": {\n "prefix": "ply-input",\n "scope": "html",\n "body": [\n "<ply-input-group>",\n " <ply-label>${1:Label}</ply-label>",\n " <input ply-input [(ngModel)]=\\"${2:value}\\" autocomplete=\\"off\\" />",\n "</ply-input-group>"\n ],\n "description": "Native input with ply-input. Install: npx ply-ui-cli add input --yes"\n },\n "Ply textarea": {\n "prefix": "ply-textarea",\n "scope": "html",\n "body": [\n "<ply-input-group>",\n " <ply-label>${1:Notes}</ply-label>",\n " <textarea ply-textarea rows=\\"4\\" [(ngModel)]=\\"${2:notes}\\"></textarea>",\n "</ply-input-group>"\n ],\n "description": "Native textarea with ply-textarea. Install: npx ply-ui-cli add input --yes"\n },\n "Ply combobox": {\n "prefix": "ply-combobox",\n "scope": "html",\n "body": [\n "<ply-combobox",\n " label=\\"${1:Choice}\\"",\n " [options]=\\"${2:options}\\"",\n " [(ngModel)]=\\"${3:value}\\">",\n " <ng-template plyComboboxOption let-option>",\n " $0",\n " </ng-template>",\n "</ply-combobox>"\n ],\n "description": "Searchable select with optional option template. Bind ngModel on the host. Install: npx ply-ui-cli add combobox --yes"\n },\n "Ply select": {\n "prefix": "ply-select",\n "scope": "html",\n "body": [\n "<ply-custom-select",\n " label=\\"${1:Choice}\\"",\n " [options]=\\"${2:options}\\"",\n " [(ngModel)]=\\"${3:value}\\">",\n " <ng-template plySelectOption let-label=\\"label\\">",\n " $0",\n " </ng-template>",\n "</ply-custom-select>"\n ],\n "description": "Custom select CVA host with optional option template. Install: npx ply-ui-cli add custom-select --yes"\n },\n "Ply toggle": {\n "prefix": "ply-toggle",\n "scope": "html",\n "body": [\n "<div class=\\"flex items-center justify-between gap-[12px] rounded-lg border border-slate-200 px-[16px] py-[12px] dark:border-slate-700\\">",\n " <div>",\n " <p class=\\"m-0! text-sm font-medium text-slate-900 dark:text-white\\">${1:Product emails}</p>",\n " <p class=\\"mb-0! mt-[2px] text-xs text-slate-500\\">${2:Occasional tips.}</p>",\n " </div>",\n " <ply-toggle [(ngModel)]=\\"${3:enabled}\\"></ply-toggle>",\n "</div>"\n ],\n "description": "Toggle CVA host. Install: npx ply-ui-cli add toggle --yes"\n },\n "Ply tags input": {\n "prefix": "ply-tags-input",\n "scope": "html",\n "body": [\n "<ply-label>${1:Tags}</ply-label>",\n "<ply-tags-input class=\\"mt-[4px]\\" [(ngModel)]=\\"${2:tags}\\" placeholder=\\"${3:Add a tag\u2026}\\"></ply-tags-input>"\n ],\n "description": "Chip tags CVA host. Install: npx ply-ui-cli add tags-input --yes"\n },\n "Ply checkbox": {\n "prefix": "ply-checkbox",\n "scope": "html",\n "body": [\n "<ply-checkbox [(ngModel)]=\\"${1:checked}\\">${2:Remember me}</ply-checkbox>"\n ],\n "description": "Checkbox. Install: npx ply-ui-cli add checkbox --yes"\n },\n "Ply chat": {\n "prefix": "ply-chat",\n "scope": "html",\n "body": [\n "<ply-chat",\n " [messages]=\\"${1:messages}()\\"",\n " [streaming]=\\"${2:streaming}()\\"",\n " placeholder=\\"${3:Ask\u2026}\\"",\n " (send)=\\"${4:onSend}($event)\\"",\n " (stop)=\\"${5:stop}()\\">",\n "</ply-chat>"\n ],\n "description": "AI chat kit. Install: npx ply-ui-cli add chat --yes"\n },\n "Ply data table": {\n "prefix": "ply-data-table",\n "scope": "html",\n "body": [\n "<ply-data-table",\n " [columns]=\\"${1:columns}\\"",\n " [data]=\\"${2:rows}\\"",\n " [loading]=\\"${3:loading}()\\"",\n " serverSide",\n " sortable",\n " pageable",\n " selectable",\n " bordered",\n " hoverable",\n " [pageSize]=\\"${4:10}\\"",\n " [totalItems]=\\"${5:total}\\"",\n " [(selected)]=\\"${6:selected}\\"",\n " (pageChange)=\\"${7:fetch}($event)\\"",\n " (sortChange)=\\"${8:onSort}($event)\\">",\n " <ng-template plyTableCell=\\"${9:status}\\" let-value=\\"value\\">",\n " $0",\n " </ng-template>",\n "</ply-data-table>"\n ],\n "description": "Pro data table with optional cell templates. Install: npx ply-ui-cli add data-table --yes"\n },\n "Ply tabs": {\n "prefix": "ply-tabs",\n "scope": "html",\n "body": [\n "<ply-tabs [defaultTab]=\\"${1:0}\\">",\n " <ply-tab label=\\"${2:Overview}\\"><ply-tab-body>$0</ply-tab-body></ply-tab>",\n " <ply-tab label=\\"${3:Activity}\\"><ply-tab-body></ply-tab-body></ply-tab>",\n "</ply-tabs>"\n ],\n "description": "Tabs. Install: npx ply-ui-cli add tabs --yes"\n },\n "Ply alert": {\n "prefix": "ply-alert",\n "scope": "html",\n "body": [\n "<ply-alert color=\\"${1:info}\\">${2:Saved.}</ply-alert>"\n ],\n "description": "Alert. Install: npx ply-ui-cli add alert --yes"\n },\n "Ply badge": {\n "prefix": "ply-badge",\n "scope": "html",\n "body": [\n "<ply-badge color=\\"${1:primary-light}\\" size=\\"sm\\">${2:New}</ply-badge>"\n ],\n "description": "Badge. Install: npx ply-ui-cli add badge --yes"\n },\n "Ply skeleton": {\n "prefix": "ply-skeleton",\n "scope": "html",\n "body": [\n "<ply-skeleton variant=\\"${1:rectangular}\\" width=\\"${2:100%}\\" height=\\"${3:16rem}\\"></ply-skeleton>"\n ],\n "description": "Skeleton placeholder. Install: npx ply-ui-cli add skeleton --yes"\n },\n "Ply empty state": {\n "prefix": "ply-empty-state",\n "scope": "html",\n "body": [\n "<ply-empty-state title=\\"${1:Nothing here}\\" description=\\"${2:Try a different filter.}\\"></ply-empty-state>"\n ],\n "description": "Empty state. Install: npx ply-ui-cli add empty-state --yes"\n },\n "Ply toast": {\n "prefix": "ply-toast",\n "scope": "typescript",\n "body": [\n "this.toast.success(\'${1:Saved}\', { duration: ${2:2500} });"\n ],\n "description": "Toast via ToastService. Install: npx ply-ui-cli add toast --yes. Do not add ply-toast-container to templates."\n },\n "Ply icon": {\n "prefix": "ply-icon",\n "scope": "html",\n "body": [\n "<ply-icon name=\\"${1:chevron-right}\\" class=\\"h-4 w-4 stroke-slate-500 dark:stroke-slate-300\\"></ply-icon>"\n ],\n "description": "Outline icon (stroke-*). Filled: [filled]=\\"true\\" + text-*. Install: npx ply-ui-cli add icon --yes"\n },\n "Ply standalone component": {\n "prefix": "ply-standalone",\n "scope": "typescript",\n "body": [\n "import { ChangeDetectionStrategy, Component, input, output } from \'@angular/core\';",\n "",\n "@Component({",\n " selector: \'app-${1:feature}\',",\n " changeDetection: ChangeDetectionStrategy.OnPush,",\n " template: `",\n " <ply-card>",\n " <ply-card-body class=\\"p-[16px]\\">$0</ply-card-body>",\n " </ply-card>",\n " `,",\n "})",\n "export class ${2:Feature}Component {",\n " readonly title = input(\'${3:Title}\');",\n " readonly saved = output<void>();",\n "}"\n ],\n "description": "Standalone OnPush component with signal input/output and a Ply card"\n },\n "Ply i18n": {\n "prefix": "ply-i18n",\n "scope": "typescript",\n "body": [\n "provideBaseUiI18n({",\n " close: \'${1:Fermer}\',",\n " noDataAvailable: \'${2:Aucune donn\xE9e}\',",\n " noResults: \'${3:Aucun r\xE9sultat}\'",\n "})"\n ],\n "description": "Chrome-string dictionary for empty states, paginator, and dialog close. Install: npx ply-ui-cli add i18n --yes"\n },\n "Ply config": {\n "prefix": "ply-config",\n "scope": "typescript",\n "body": [\n "provideBaseUI({",\n " iconPath: \'${1:assets/icons.svg}\',",\n " filledIconPath: \'${2:assets/icons-filled.svg}\',",\n " defaultSize: ${3:20}",\n "})"\n ],\n "description": "Library defaults for icon sprites and default icon size. Install: npx ply-ui-cli add config --yes"\n },\n "Ply harness": {\n "prefix": "ply-harness",\n "scope": "typescript",\n "body": [\n "import { TestbedHarnessEnvironment } from \'@angular/cdk/testing/testbed\';",\n "import { DataTableHarness } from \'./components/data-table/data-table.harness\';",\n "import { DialogHarness } from \'./components/dialog/dialog.harness\';",\n "",\n "const loader = TestbedHarnessEnvironment.loader(fixture);",\n "const table = await loader.getHarness(DataTableHarness);",\n "expect(await table.getRowCount()).toBe(${1:3});",\n "",\n "const dialogs = TestbedHarnessEnvironment.documentRootLoader(fixture);",\n "const dialog = await dialogs.getHarness(DialogHarness);"\n ],\n "description": "CDK ComponentHarness for copied Base UI data-table. Dialog overlays use documentRootLoader. Copied with the component \u2014 not a separate registry item."\n }\n}\n';
|
|
70
90
|
|
|
71
91
|
// src/brand.ts
|
|
@@ -86,31 +106,46 @@ var BASE_CSS = `/* Ply global styles \u2014 ${import_registry.SITE_URL} */
|
|
|
86
106
|
* just to change brand color.
|
|
87
107
|
*/
|
|
88
108
|
:root {
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
--
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
--
|
|
103
|
-
--radius: var(--
|
|
109
|
+
--ply-background: #ffffff;
|
|
110
|
+
--ply-foreground: #0f172a;
|
|
111
|
+
--ply-primary: #2563eb;
|
|
112
|
+
--ply-primary-hover: #1d4ed8;
|
|
113
|
+
--ply-primary-active: #1e40af;
|
|
114
|
+
--ply-primary-foreground: #ffffff;
|
|
115
|
+
--ply-muted: #e2e8f0;
|
|
116
|
+
--ply-muted-foreground: #334155;
|
|
117
|
+
--ply-destructive: #dc2626;
|
|
118
|
+
--ply-destructive-hover: #b91c1c;
|
|
119
|
+
--ply-destructive-foreground: #ffffff;
|
|
120
|
+
--ply-border: #cbd5e1;
|
|
121
|
+
--ply-ring: #2563eb;
|
|
122
|
+
--ply-radius: 0.5rem;
|
|
123
|
+
--radius: var(--ply-radius);
|
|
124
|
+
/* Compat for components copied before the Ply rename. */
|
|
125
|
+
--base-background: var(--ply-background);
|
|
126
|
+
--base-foreground: var(--ply-foreground);
|
|
127
|
+
--base-primary: var(--ply-primary);
|
|
128
|
+
--base-primary-hover: var(--ply-primary-hover);
|
|
129
|
+
--base-primary-active: var(--ply-primary-active);
|
|
130
|
+
--base-primary-foreground: var(--ply-primary-foreground);
|
|
131
|
+
--base-muted: var(--ply-muted);
|
|
132
|
+
--base-muted-foreground: var(--ply-muted-foreground);
|
|
133
|
+
--base-destructive: var(--ply-destructive);
|
|
134
|
+
--base-destructive-hover: var(--ply-destructive-hover);
|
|
135
|
+
--base-destructive-foreground: var(--ply-destructive-foreground);
|
|
136
|
+
--base-border: var(--ply-border);
|
|
137
|
+
--base-ring: var(--ply-ring);
|
|
138
|
+
--base-radius: var(--ply-radius);
|
|
104
139
|
}
|
|
105
140
|
|
|
106
141
|
.dark {
|
|
107
|
-
--
|
|
108
|
-
--
|
|
109
|
-
--
|
|
110
|
-
--
|
|
111
|
-
--
|
|
112
|
-
--
|
|
113
|
-
--
|
|
142
|
+
--ply-background: #0f172a;
|
|
143
|
+
--ply-foreground: #f8fafc;
|
|
144
|
+
--ply-muted: #1e293b;
|
|
145
|
+
--ply-muted-foreground: #e2e8f0;
|
|
146
|
+
--ply-border: #334155;
|
|
147
|
+
--ply-primary-foreground: #ffffff;
|
|
148
|
+
--ply-destructive-foreground: #ffffff;
|
|
114
149
|
}
|
|
115
150
|
|
|
116
151
|
@keyframes fade-in-up {
|
|
@@ -204,8 +239,8 @@ async function downloadSprite(name, targetDir) {
|
|
|
204
239
|
const res = await fetch(`${import_registry.SITE_URL}/assets/${name}`);
|
|
205
240
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
206
241
|
const body = Buffer.from(await res.arrayBuffer());
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
import_fs3.default.mkdirSync(targetDir, { recursive: true });
|
|
243
|
+
import_fs3.default.writeFileSync(import_path2.default.join(targetDir, name), body);
|
|
209
244
|
return true;
|
|
210
245
|
} catch (err) {
|
|
211
246
|
console.warn(`
|
|
@@ -214,14 +249,14 @@ async function downloadSprite(name, targetDir) {
|
|
|
214
249
|
}
|
|
215
250
|
}
|
|
216
251
|
function writeEditorFile(dest, content, label) {
|
|
217
|
-
if (
|
|
252
|
+
if (import_fs3.default.existsSync(dest)) return;
|
|
218
253
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
254
|
+
import_fs3.default.mkdirSync(import_path2.default.dirname(dest), { recursive: true });
|
|
255
|
+
import_fs3.default.writeFileSync(dest, content, "utf8");
|
|
221
256
|
} catch (err) {
|
|
222
257
|
console.warn(
|
|
223
258
|
`
|
|
224
|
-
\u26A0 Could not write ${label} (${err.message}). Copy it from ${import_registry.SITE_URL}
|
|
259
|
+
\u26A0 Could not write ${label} (${err.message}). Copy it from ${import_registry.SITE_URL}/${label.endsWith(".code-snippets") ? DOCS_SNIPPETS : DOCS_CURSOR_RULE}.`
|
|
225
260
|
);
|
|
226
261
|
}
|
|
227
262
|
}
|
|
@@ -255,8 +290,8 @@ async function init(options = {}) {
|
|
|
255
290
|
console.log("Initialization cancelled.");
|
|
256
291
|
return;
|
|
257
292
|
}
|
|
258
|
-
const spinner = (0, import_vendor.ora)(
|
|
259
|
-
const configPath = import_path2.default.resolve(process.cwd(),
|
|
293
|
+
const spinner = (0, import_vendor.ora)(`Writing ${CONFIG_FILE}...`).start();
|
|
294
|
+
const configPath = import_path2.default.resolve(process.cwd(), CONFIG_FILE);
|
|
260
295
|
const config = {
|
|
261
296
|
$schema: "https://ply-ui.com/schema.json",
|
|
262
297
|
style: "scss",
|
|
@@ -271,41 +306,45 @@ async function init(options = {}) {
|
|
|
271
306
|
utils: "src/app/utils"
|
|
272
307
|
}
|
|
273
308
|
};
|
|
274
|
-
|
|
309
|
+
import_fs3.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
275
310
|
const lockPath = import_path2.default.resolve(process.cwd(), LOCKFILE_NAME);
|
|
276
|
-
if (!
|
|
311
|
+
if (!import_fs3.default.existsSync(lockPath)) {
|
|
277
312
|
writeLockFile(readLockFile());
|
|
278
313
|
}
|
|
279
314
|
spinner.text = "Writing global styles...";
|
|
280
315
|
const stylesPath = import_path2.default.resolve(process.cwd(), response.styles);
|
|
281
316
|
const stylesDir = import_path2.default.dirname(stylesPath);
|
|
282
|
-
const
|
|
283
|
-
const importLine = `@import '
|
|
317
|
+
const cssPath = import_path2.default.join(stylesDir, CSS_FILE);
|
|
318
|
+
const importLine = `@import './${CSS_FILE}';`;
|
|
319
|
+
const legacyImport = `@import './${LEGACY_CSS_FILE}';`;
|
|
284
320
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
if (!stylesContent.includes(importLine)) {
|
|
290
|
-
|
|
321
|
+
import_fs3.default.mkdirSync(stylesDir, { recursive: true });
|
|
322
|
+
import_fs3.default.writeFileSync(cssPath, BASE_CSS, "utf8");
|
|
323
|
+
if (import_fs3.default.existsSync(stylesPath)) {
|
|
324
|
+
let stylesContent = import_fs3.default.readFileSync(stylesPath, "utf8");
|
|
325
|
+
if (stylesContent.includes(legacyImport) && !stylesContent.includes(importLine)) {
|
|
326
|
+
stylesContent = stylesContent.replace(legacyImport, importLine);
|
|
327
|
+
import_fs3.default.writeFileSync(stylesPath, stylesContent, "utf8");
|
|
328
|
+
} else if (!stylesContent.includes(importLine)) {
|
|
329
|
+
import_fs3.default.writeFileSync(stylesPath, `${importLine}
|
|
291
330
|
${stylesContent}`, "utf8");
|
|
292
331
|
}
|
|
293
332
|
} else {
|
|
294
|
-
|
|
333
|
+
import_fs3.default.writeFileSync(stylesPath, `${importLine}
|
|
295
334
|
`, "utf8");
|
|
296
335
|
console.warn(`
|
|
297
336
|
\u26A0 ${response.styles} did not exist \u2014 created it. Make sure it is listed in the "styles" array of angular.json.`);
|
|
298
337
|
}
|
|
299
338
|
} catch (error) {
|
|
300
339
|
console.warn(`
|
|
301
|
-
\u26A0 Could not update global styles (${error.message}). Add "${importLine}" to ${response.styles} manually and create
|
|
340
|
+
\u26A0 Could not update global styles (${error.message}). Add "${importLine}" to ${response.styles} manually and create ${CSS_FILE} next to it.`);
|
|
302
341
|
}
|
|
303
342
|
spinner.text = "Setting up icon sprites...";
|
|
304
343
|
let assetsDir = import_path2.default.resolve(process.cwd(), "src/assets");
|
|
305
344
|
try {
|
|
306
345
|
const angularJsonPath = import_path2.default.resolve(process.cwd(), "angular.json");
|
|
307
|
-
if (
|
|
308
|
-
const angularJson = JSON.parse(
|
|
346
|
+
if (import_fs3.default.existsSync(angularJsonPath)) {
|
|
347
|
+
const angularJson = JSON.parse(import_fs3.default.readFileSync(angularJsonPath, "utf8"));
|
|
309
348
|
const picked = pickAngularProject(angularJson);
|
|
310
349
|
const buildOptions = picked?.project?.architect?.build?.options;
|
|
311
350
|
if (buildOptions) {
|
|
@@ -331,7 +370,7 @@ ${stylesContent}`, "utf8");
|
|
|
331
370
|
}
|
|
332
371
|
}
|
|
333
372
|
buildOptions.assets = assets;
|
|
334
|
-
|
|
373
|
+
import_fs3.default.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2), "utf8");
|
|
335
374
|
}
|
|
336
375
|
} else {
|
|
337
376
|
console.warn(`
|
|
@@ -350,14 +389,14 @@ ${stylesContent}`, "utf8");
|
|
|
350
389
|
}
|
|
351
390
|
spinner.text = "Writing editor rules and snippets...";
|
|
352
391
|
writeEditorFile(
|
|
353
|
-
import_path2.default.resolve(process.cwd(),
|
|
392
|
+
import_path2.default.resolve(process.cwd(), CURSOR_RULE_REL),
|
|
354
393
|
CURSOR_RULE,
|
|
355
|
-
|
|
394
|
+
CURSOR_RULE_REL
|
|
356
395
|
);
|
|
357
396
|
writeEditorFile(
|
|
358
|
-
import_path2.default.resolve(process.cwd(),
|
|
397
|
+
import_path2.default.resolve(process.cwd(), SNIPPETS_REL),
|
|
359
398
|
CODE_SNIPPETS,
|
|
360
|
-
|
|
399
|
+
SNIPPETS_REL
|
|
361
400
|
);
|
|
362
401
|
spinner.succeed(`Project initialized! Add your first component with "${CLI_NPX} add button".`);
|
|
363
402
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -33,13 +33,13 @@ __export(update_exports, {
|
|
|
33
33
|
update: () => update
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(update_exports);
|
|
36
|
-
var
|
|
36
|
+
var import_fs6 = __toESM(require("fs"));
|
|
37
37
|
var import_path6 = __toESM(require("path"));
|
|
38
38
|
var import_vendor = require("../vendor");
|
|
39
39
|
|
|
40
40
|
// src/config.ts
|
|
41
|
-
var
|
|
42
|
-
var
|
|
41
|
+
var import_fs2 = __toESM(require("fs"));
|
|
42
|
+
var import_path2 = __toESM(require("path"));
|
|
43
43
|
|
|
44
44
|
// src/brand.ts
|
|
45
45
|
var CLI_BIN = "ply-ui-cli";
|
|
@@ -47,44 +47,63 @@ var CLI_NPX = `npx ${CLI_BIN}`;
|
|
|
47
47
|
var MCP_PACKAGE = "ply-ui-mcp";
|
|
48
48
|
var MCP_NPX = `npx -y ${MCP_PACKAGE}`;
|
|
49
49
|
|
|
50
|
+
// src/paths.ts
|
|
51
|
+
var import_fs = __toESM(require("fs"));
|
|
52
|
+
var import_path = __toESM(require("path"));
|
|
53
|
+
var CONFIG_FILE = "ply-ui.json";
|
|
54
|
+
var LEGACY_CONFIG_FILE = "base-ui.json";
|
|
55
|
+
var LOCKFILE_NAME = "ply-ui-lock.json";
|
|
56
|
+
var LEGACY_LOCKFILE_NAME = "base-ui-lock.json";
|
|
57
|
+
var SNAPSHOT_ROOT = ".ply-ui/snapshots";
|
|
58
|
+
function firstExisting(cwd, names) {
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
const resolved = import_path.default.resolve(cwd, name);
|
|
61
|
+
if (import_fs.default.existsSync(resolved)) return resolved;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function configFilePath(cwd = process.cwd()) {
|
|
66
|
+
return firstExisting(cwd, [CONFIG_FILE, LEGACY_CONFIG_FILE]);
|
|
67
|
+
}
|
|
68
|
+
function lockfilePath(cwd = process.cwd()) {
|
|
69
|
+
return firstExisting(cwd, [LOCKFILE_NAME, LEGACY_LOCKFILE_NAME]) || import_path.default.resolve(cwd, LOCKFILE_NAME);
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
// src/config.ts
|
|
51
73
|
function readConfig() {
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
54
|
-
console.error(
|
|
74
|
+
const resolved = configFilePath();
|
|
75
|
+
if (!resolved) {
|
|
76
|
+
console.error(
|
|
77
|
+
`${CONFIG_FILE} not found (also looked for ${LEGACY_CONFIG_FILE}). Please run "${CLI_NPX} init" first.`
|
|
78
|
+
);
|
|
55
79
|
return null;
|
|
56
80
|
}
|
|
57
81
|
try {
|
|
58
|
-
const config = JSON.parse(
|
|
82
|
+
const config = JSON.parse(import_fs2.default.readFileSync(resolved, "utf8"));
|
|
59
83
|
if (!config || !config.aliases || !config.aliases.components) {
|
|
60
|
-
throw new Error(
|
|
84
|
+
throw new Error(`Missing aliases.components in ${import_path2.default.basename(resolved)}`);
|
|
61
85
|
}
|
|
62
86
|
return config;
|
|
63
87
|
} catch (err) {
|
|
64
|
-
console.error(`Error reading
|
|
88
|
+
console.error(`Error reading ${import_path2.default.basename(resolved)}: ${err.message}`);
|
|
65
89
|
return null;
|
|
66
90
|
}
|
|
67
91
|
}
|
|
68
92
|
|
|
69
93
|
// src/lockfile.ts
|
|
70
|
-
var
|
|
71
|
-
var import_path2 = __toESM(require("path"));
|
|
94
|
+
var import_fs3 = __toESM(require("fs"));
|
|
72
95
|
var import_crypto = __toESM(require("crypto"));
|
|
73
|
-
var LOCKFILE_NAME = "base-ui-lock.json";
|
|
74
96
|
var LOCKFILE_VERSION = 1;
|
|
75
97
|
function hashContent(content) {
|
|
76
98
|
return import_crypto.default.createHash("sha256").update(content, "utf8").digest("hex");
|
|
77
99
|
}
|
|
78
|
-
function lockfilePath() {
|
|
79
|
-
return import_path2.default.resolve(process.cwd(), LOCKFILE_NAME);
|
|
80
|
-
}
|
|
81
100
|
function readLockFile() {
|
|
82
101
|
const p = lockfilePath();
|
|
83
|
-
if (!
|
|
102
|
+
if (!import_fs3.default.existsSync(p)) {
|
|
84
103
|
return { version: LOCKFILE_VERSION, components: {} };
|
|
85
104
|
}
|
|
86
105
|
try {
|
|
87
|
-
const parsed = JSON.parse(
|
|
106
|
+
const parsed = JSON.parse(import_fs3.default.readFileSync(p, "utf8"));
|
|
88
107
|
if (parsed && typeof parsed === "object" && parsed.components) {
|
|
89
108
|
return { version: parsed.version ?? LOCKFILE_VERSION, components: parsed.components };
|
|
90
109
|
}
|
|
@@ -93,7 +112,7 @@ function readLockFile() {
|
|
|
93
112
|
return { version: LOCKFILE_VERSION, components: {} };
|
|
94
113
|
}
|
|
95
114
|
function writeLockFile(lock) {
|
|
96
|
-
|
|
115
|
+
import_fs3.default.writeFileSync(lockfilePath(), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
// src/path-safety.ts
|
|
@@ -109,33 +128,32 @@ var import_registry = require("../registry");
|
|
|
109
128
|
var import_sync = require("../sync");
|
|
110
129
|
|
|
111
130
|
// src/snapshot.ts
|
|
112
|
-
var
|
|
131
|
+
var import_fs4 = __toESM(require("fs"));
|
|
113
132
|
var import_path4 = __toESM(require("path"));
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return import_path4.default.resolve(process.cwd(), SNAPSHOT_ROOT, component, file);
|
|
133
|
+
function snapshotPath(component, file, root) {
|
|
134
|
+
return import_path4.default.resolve(process.cwd(), root, component, file);
|
|
117
135
|
}
|
|
118
136
|
function writeSnapshot(component, file, content) {
|
|
119
137
|
if (!isSafeFileName(file) || !isSafeFileName(component)) return;
|
|
120
|
-
const dest = snapshotPath(component, file);
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
const dest = snapshotPath(component, file, SNAPSHOT_ROOT);
|
|
139
|
+
import_fs4.default.mkdirSync(import_path4.default.dirname(dest), { recursive: true });
|
|
140
|
+
import_fs4.default.writeFileSync(dest, content, "utf8");
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
// src/merge.ts
|
|
126
|
-
var
|
|
144
|
+
var import_fs5 = __toESM(require("fs"));
|
|
127
145
|
var import_os = __toESM(require("os"));
|
|
128
146
|
var import_path5 = __toESM(require("path"));
|
|
129
147
|
var import_child_process = require("child_process");
|
|
130
148
|
function mergeThreeWay(args) {
|
|
131
|
-
const dir =
|
|
149
|
+
const dir = import_fs5.default.mkdtempSync(import_path5.default.join(import_os.default.tmpdir(), "base-ui-merge-"));
|
|
132
150
|
const ancestorPath = import_path5.default.join(dir, "ancestor");
|
|
133
151
|
const localPath = import_path5.default.join(dir, "local");
|
|
134
152
|
const upstreamPath = import_path5.default.join(dir, "upstream");
|
|
135
153
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
import_fs5.default.writeFileSync(ancestorPath, args.ancestor, "utf8");
|
|
155
|
+
import_fs5.default.writeFileSync(localPath, args.local, "utf8");
|
|
156
|
+
import_fs5.default.writeFileSync(upstreamPath, args.upstream, "utf8");
|
|
139
157
|
const result = (0, import_child_process.spawnSync)(
|
|
140
158
|
"git",
|
|
141
159
|
["merge-file", "-p", "--diff3", localPath, ancestorPath, upstreamPath],
|
|
@@ -151,7 +169,7 @@ function mergeThreeWay(args) {
|
|
|
151
169
|
const text = result.stdout ?? "";
|
|
152
170
|
return { clean: result.status === 0, text };
|
|
153
171
|
} finally {
|
|
154
|
-
|
|
172
|
+
import_fs5.default.rmSync(dir, { recursive: true, force: true });
|
|
155
173
|
}
|
|
156
174
|
}
|
|
157
175
|
function conflictMarkers(args) {
|
|
@@ -253,8 +271,8 @@ async function update(components, options = {}) {
|
|
|
253
271
|
console.warn(`\u26A0 Skipping unsafe file path from registry: ${result.name}/${file.path}`);
|
|
254
272
|
continue;
|
|
255
273
|
}
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
import_fs6.default.mkdirSync(import_path6.default.dirname(localPath), { recursive: true });
|
|
275
|
+
import_fs6.default.writeFileSync(localPath, file.upstreamContent ?? "", "utf8");
|
|
258
276
|
lock.components[result.name].files[file.path] = hashContent(file.upstreamContent ?? "");
|
|
259
277
|
writeSnapshot(result.name, file.path, file.upstreamContent ?? "");
|
|
260
278
|
lockChanged = true;
|
|
@@ -273,8 +291,8 @@ async function update(components, options = {}) {
|
|
|
273
291
|
continue;
|
|
274
292
|
}
|
|
275
293
|
if (file.status === "upstream-changed" || file.status === "new-upstream") {
|
|
276
|
-
|
|
277
|
-
|
|
294
|
+
import_fs6.default.mkdirSync(import_path6.default.dirname(localPath), { recursive: true });
|
|
295
|
+
import_fs6.default.writeFileSync(localPath, file.upstreamContent ?? "", "utf8");
|
|
278
296
|
lock.components[result.name].files[file.path] = hashContent(file.upstreamContent ?? "");
|
|
279
297
|
writeSnapshot(result.name, file.path, file.upstreamContent ?? "");
|
|
280
298
|
lockChanged = true;
|
|
@@ -307,7 +325,7 @@ async function update(components, options = {}) {
|
|
|
307
325
|
local: file.localContent ?? "",
|
|
308
326
|
upstream: file.upstreamContent ?? ""
|
|
309
327
|
});
|
|
310
|
-
|
|
328
|
+
import_fs6.default.writeFileSync(localPath, merged.text, "utf8");
|
|
311
329
|
lock.components[result.name].files[file.path] = hashContent(merged.text);
|
|
312
330
|
writeSnapshot(result.name, file.path, merged.text);
|
|
313
331
|
lockChanged = true;
|
|
@@ -322,14 +340,14 @@ async function update(components, options = {}) {
|
|
|
322
340
|
}
|
|
323
341
|
}
|
|
324
342
|
} else if (choice === "upstream") {
|
|
325
|
-
|
|
343
|
+
import_fs6.default.writeFileSync(localPath, file.upstreamContent ?? "", "utf8");
|
|
326
344
|
lock.components[result.name].files[file.path] = hashContent(file.upstreamContent ?? "");
|
|
327
345
|
writeSnapshot(result.name, file.path, file.upstreamContent ?? "");
|
|
328
346
|
lockChanged = true;
|
|
329
347
|
conflictsToUpstream++;
|
|
330
348
|
console.log(` \u2191 ${result.name}/${file.path} (took upstream, your edits were overwritten)`);
|
|
331
349
|
} else if (choice === "save-aside") {
|
|
332
|
-
|
|
350
|
+
import_fs6.default.writeFileSync(`${localPath}.upstream`, file.upstreamContent ?? "", "utf8");
|
|
333
351
|
conflictsSavedAside++;
|
|
334
352
|
console.log(` \u21C4 ${result.name}/${file.path}.upstream (merge by hand \u2014 your file was left untouched)`);
|
|
335
353
|
} else {
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ function printHelp() {
|
|
|
34
34
|
console.log(`Usage: ${CLI_BIN} <command> [options]
|
|
35
35
|
|
|
36
36
|
Commands:
|
|
37
|
-
init Initialize your project (
|
|
37
|
+
init Initialize your project (ply-ui.json, styles, icons, Cursor rules, snippets)
|
|
38
38
|
add [components...] Add components; omit names to pick from the catalog
|
|
39
39
|
list List all available components (free and pro)
|
|
40
40
|
diff [components...] Show upstream changes for installed components since you added them
|
|
@@ -108,7 +108,7 @@ function parseArgs(argv) {
|
|
|
108
108
|
async function main() {
|
|
109
109
|
const { command, positionals, flags } = parseArgs(process.argv.slice(2));
|
|
110
110
|
if (flags.version && !command) {
|
|
111
|
-
console.log("1.1.
|
|
111
|
+
console.log("1.1.125");
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
if (flags.help || !command) {
|
|
@@ -158,7 +158,7 @@ async function main() {
|
|
|
158
158
|
}
|
|
159
159
|
case "--version":
|
|
160
160
|
case "-V":
|
|
161
|
-
console.log("1.1.
|
|
161
|
+
console.log("1.1.125");
|
|
162
162
|
return;
|
|
163
163
|
default:
|
|
164
164
|
console.error(`Unknown command: ${command}`);
|
package/dist/sync.js
CHANGED
|
@@ -45,6 +45,13 @@ var import_registry = require("./registry");
|
|
|
45
45
|
|
|
46
46
|
// src/lockfile.ts
|
|
47
47
|
var import_crypto = __toESM(require("crypto"));
|
|
48
|
+
|
|
49
|
+
// src/paths.ts
|
|
50
|
+
var LOCKFILE_NAME = "ply-ui-lock.json";
|
|
51
|
+
var SNAPSHOT_ROOT = ".ply-ui/snapshots";
|
|
52
|
+
var LEGACY_SNAPSHOT_ROOT = ".base-ui/snapshots";
|
|
53
|
+
|
|
54
|
+
// src/lockfile.ts
|
|
48
55
|
function hashContent(content) {
|
|
49
56
|
return import_crypto.default.createHash("sha256").update(content, "utf8").digest("hex");
|
|
50
57
|
}
|
|
@@ -52,16 +59,18 @@ function hashContent(content) {
|
|
|
52
59
|
// src/snapshot.ts
|
|
53
60
|
var import_fs = __toESM(require("fs"));
|
|
54
61
|
var import_path = __toESM(require("path"));
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return import_path.default.resolve(process.cwd(), SNAPSHOT_ROOT, component, file);
|
|
62
|
+
function snapshotPath(component, file, root) {
|
|
63
|
+
return import_path.default.resolve(process.cwd(), root, component, file);
|
|
58
64
|
}
|
|
59
65
|
function readSnapshot(component, file, expectedHash) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
for (const root of [SNAPSHOT_ROOT, LEGACY_SNAPSHOT_ROOT]) {
|
|
67
|
+
const dest = snapshotPath(component, file, root);
|
|
68
|
+
if (!import_fs.default.existsSync(dest)) continue;
|
|
69
|
+
const content = import_fs.default.readFileSync(dest, "utf8");
|
|
70
|
+
if (expectedHash && hashContent(content) !== expectedHash) continue;
|
|
71
|
+
return content;
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
// src/brand.ts
|
|
@@ -100,7 +109,7 @@ async function computeComponentDiff(name, componentsRoot, lock, index) {
|
|
|
100
109
|
return {
|
|
101
110
|
name,
|
|
102
111
|
files: [],
|
|
103
|
-
error: `not tracked in ${
|
|
112
|
+
error: `not tracked in ${LOCKFILE_NAME} \u2014 run "${CLI_NPX} add ${name} --overwrite" to start tracking it.`
|
|
104
113
|
};
|
|
105
114
|
}
|
|
106
115
|
const indexEntry = index.find((i) => i.name === name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ply-ui-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.125",
|
|
4
4
|
"description": "Ply CLI — add Angular + Tailwind CSS components to your project (shadcn-style, code is copied in and owned by you). Not MUI Base UI.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"publishConfig": {
|