jixoai-ui 0.2.0 → 0.4.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/README.md +34 -1
- package/bin/jixoai-ui.mjs +341 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,11 +16,42 @@ The official jixoai design-language CLI. It **shares shadcn's
|
|
|
16
16
|
```bash
|
|
17
17
|
npx jixoai-ui init --hue 160 # namespace + config + theme + hue, one shot
|
|
18
18
|
npx jixoai-ui add toc # = shadcn add @jixoai/toc, hue re-applied
|
|
19
|
+
npx jixoai-ui add effects # group alias: every ui item in the group
|
|
20
|
+
npx jixoai-ui add effects/glass # one member (membership validated)
|
|
21
|
+
npx jixoai-ui add llms-txt # AI export: llms.txt / llms-full.txt / page .md
|
|
19
22
|
npx jixoai-ui upgrade # refresh locked items + run upgrade tasks
|
|
20
23
|
npx jixoai-ui hue 165 # retheme by changing one number
|
|
21
24
|
npx jixoai-ui config # print the resolved jixoai config
|
|
22
25
|
```
|
|
23
26
|
|
|
27
|
+
## Group aliases
|
|
28
|
+
|
|
29
|
+
`add` accepts three argument forms (effect-attachments Lane H):
|
|
30
|
+
|
|
31
|
+
- **Item name** — `npx jixoai-ui add glass`: as-is, any registry type. An
|
|
32
|
+
exact item name always wins when an item and a group id collide.
|
|
33
|
+
- **Group id** — `npx jixoai-ui add effects`: expands to EVERY
|
|
34
|
+
`registry:ui` item whose `meta.group` is `effects`, in registry order,
|
|
35
|
+
and prints the expansion (`jixoai-ui: effects → press-button, glass`).
|
|
36
|
+
The expansion is what installs AND what enters `jixoai-ui.lock` — the
|
|
37
|
+
lock records item names, never group ids.
|
|
38
|
+
- **Scoped member** — `npx jixoai-ui add effects/glass`: resolves to
|
|
39
|
+
`glass` after validating the group actually owns the item; a wrong
|
|
40
|
+
group (`add effects/toc`) exits non-zero naming the item's real group
|
|
41
|
+
and the requested one.
|
|
42
|
+
|
|
43
|
+
Groups are an ADD-time convenience: `adopt` and `upgrade` stay
|
|
44
|
+
item-name-only. Membership comes from `/r/registry.json` (the registry
|
|
45
|
+
index) — a custom registry URL without an index keeps the standing
|
|
46
|
+
bare-name behavior for plain adds and refuses the scoped form.
|
|
47
|
+
|
|
48
|
+
`llms-txt` installs `vite-plugins/llms-txt.mjs` — the build-time
|
|
49
|
+
llms.txt/llms-full.txt/per-page-`.md` generator (llmstxt.org proposal
|
|
50
|
+
v2). Wire it ONE way: `llmsTxt()` in vite plugins for plain-build sites,
|
|
51
|
+
or `generateLlmsTxt(distDir, config)` as the last step of an orchestrated
|
|
52
|
+
build. Full law + config schema:
|
|
53
|
+
`skills/jixoai-website/references/llms-txt.md`.
|
|
54
|
+
|
|
24
55
|
Requires `components.json` (run `npx shadcn init` first in fresh projects —
|
|
25
56
|
this CLI extends shadcn's config, it never replaces it).
|
|
26
57
|
|
|
@@ -34,7 +65,9 @@ and in any shell loop.
|
|
|
34
65
|
- **Lock**: `init`/`add` record every installed item in `jixoai-ui.lock`
|
|
35
66
|
(next to `components.json`) as
|
|
36
67
|
`{ items: { [name]: { files: { [path]: sha256 } } } }`. Paths are resolved
|
|
37
|
-
through `components.json` aliases
|
|
68
|
+
through `components.json` aliases (`$lib`-rooted values resolve through
|
|
69
|
+
the project's tsconfig/jsconfig `compilerOptions.paths`, the same map
|
|
70
|
+
shadcn uses); hashes cover canonical registry content
|
|
38
71
|
(pre-hue, pre-task). A missing or empty lock fails with exit code 1 and
|
|
39
72
|
tells you to `add` first.
|
|
40
73
|
- **Refresh**: every locked item is fetched from `registries["@jixoai"]`
|
package/bin/jixoai-ui.mjs
CHANGED
|
@@ -29,11 +29,29 @@
|
|
|
29
29
|
* registry sha256 differs from the locked one, re-applies hue, then runs
|
|
30
30
|
* the idempotent upgrade tasks (bin/upgrade-tasks.mjs) — a converged
|
|
31
31
|
* second run performs zero writes.
|
|
32
|
+
*
|
|
33
|
+
* Install integrity (consumer-feedback-fixes P0-3, 2026-09-06): "successful"
|
|
34
|
+
* means VERIFIED ON DISK — an item enters the lock only when every one of
|
|
35
|
+
* its files exists at its alias-resolved path (a non-interactive shadcn run
|
|
36
|
+
* whose overwrite prompt hits EOF cancels its write phase; the item then
|
|
37
|
+
* stays unlocked with an explicit recovery warning). After every add phase
|
|
38
|
+
* the CLI also relocates files shadcn dropped into literal `src/@lib/`,
|
|
39
|
+
* `src/@ui/` and `src/vite-plugins/` directories to their alias-resolved
|
|
40
|
+
* destinations, and item-name parsing skips `--` tokens (flags never
|
|
41
|
+
* masquerade as `@jixoai/--help`).
|
|
32
42
|
*/
|
|
33
43
|
|
|
34
44
|
import { spawnSync } from "node:child_process";
|
|
35
45
|
import { createHash } from "node:crypto";
|
|
36
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
existsSync,
|
|
48
|
+
mkdirSync,
|
|
49
|
+
readdirSync,
|
|
50
|
+
readFileSync,
|
|
51
|
+
renameSync,
|
|
52
|
+
rmdirSync,
|
|
53
|
+
writeFileSync,
|
|
54
|
+
} from "node:fs";
|
|
37
55
|
import { dirname, join, relative, resolve } from "node:path";
|
|
38
56
|
import process from "node:process";
|
|
39
57
|
|
|
@@ -56,7 +74,11 @@ Commands:
|
|
|
56
74
|
lock (first upgrade then syncs to canon)
|
|
57
75
|
jixoai-ui add <item...> install registry items (delegates to
|
|
58
76
|
\`shadcn add ${NAMESPACE}/<item>\`), then
|
|
59
|
-
re-applies the brand hue
|
|
77
|
+
re-applies the brand hue. Group
|
|
78
|
+
aliases: \`add effects\` installs
|
|
79
|
+
every ui item in the group,
|
|
80
|
+
\`add effects/glass\` installs one
|
|
81
|
+
member
|
|
60
82
|
jixoai-ui upgrade refresh every locked item to the latest
|
|
61
83
|
registry content, re-apply the brand
|
|
62
84
|
hue, and run the idempotent upgrade
|
|
@@ -95,10 +117,65 @@ function ensureNamespace(config) {
|
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
|
|
120
|
+
/* ── $-alias resolution (effect-attachments Lane H, 2026-09-10) ──
|
|
121
|
+
*
|
|
122
|
+
* shadcn-svelte consumers carry `$lib`-ROOTED alias values
|
|
123
|
+
* (`"ui": "$lib/ui"` — the frozen table the clean-install harness
|
|
124
|
+
* proves). A literal `$lib` directory never exists on disk, so every
|
|
125
|
+
* alias base must first resolve through the project's
|
|
126
|
+
* tsconfig/jsconfig `compilerOptions.paths` (the same map shadcn
|
|
127
|
+
* itself resolves aliases with) before it becomes a filesystem path.
|
|
128
|
+
* Before this, `add` on such consumers installed fine but the lock
|
|
129
|
+
* found ZERO files at "(no) install path" and recorded nothing —
|
|
130
|
+
* `upgrade` went dead while the files sat in place. A base that maps
|
|
131
|
+
* nowhere keeps its literal meaning; `extends`-chained configs are a
|
|
132
|
+
* known limit (the direct paths table wins). */
|
|
133
|
+
const aliasResolverCache = new Map(); // cwd → (base → resolved base)
|
|
134
|
+
|
|
135
|
+
function tsconfigPathsFor(cwd) {
|
|
136
|
+
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
137
|
+
const path = join(cwd, name);
|
|
138
|
+
if (!existsSync(path)) continue;
|
|
139
|
+
try {
|
|
140
|
+
const paths = JSON.parse(readFileSync(path, "utf8"))?.compilerOptions?.paths;
|
|
141
|
+
if (paths && typeof paths === "object") return paths;
|
|
142
|
+
} catch {
|
|
143
|
+
// an unparseable config is not fatal — the literal base stands
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function aliasBaseResolver(cwd) {
|
|
150
|
+
if (aliasResolverCache.has(cwd)) return aliasResolverCache.get(cwd);
|
|
151
|
+
const paths = tsconfigPathsFor(cwd);
|
|
152
|
+
const star = (v) => String(Array.isArray(v) ? v[0] ?? "" : v ?? "").replace(/\*$/, "");
|
|
153
|
+
const resolveBase = (base) => {
|
|
154
|
+
if (!paths || !base.startsWith("$")) return base;
|
|
155
|
+
if (paths[base] !== undefined) return star(paths[base]);
|
|
156
|
+
const wildcards = Object.keys(paths)
|
|
157
|
+
.filter((k) => k.endsWith("/*"))
|
|
158
|
+
.map((k) => k.slice(0, -1)) // '$lib/*' → '$lib/'
|
|
159
|
+
.sort((a, b) => b.length - a.length); // longest prefix wins
|
|
160
|
+
for (const prefix of wildcards) {
|
|
161
|
+
if (base.startsWith(prefix)) return star(paths[`${prefix}*`]) + base.slice(prefix.length);
|
|
162
|
+
}
|
|
163
|
+
return base;
|
|
164
|
+
};
|
|
165
|
+
aliasResolverCache.set(cwd, resolveBase);
|
|
166
|
+
return resolveBase;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** an alias VALUE (`"$lib/ui"` / `"src/lib/ui"`) → a cwd-relative path */
|
|
170
|
+
function aliasDir(aliasValue, cwd) {
|
|
171
|
+
return resolve(cwd, aliasBaseResolver(cwd)(aliasValue));
|
|
172
|
+
}
|
|
173
|
+
|
|
98
174
|
function themeCssPath(config, cwd) {
|
|
99
175
|
const lib = config.aliases?.lib;
|
|
100
176
|
if (typeof lib !== "string") return null;
|
|
101
|
-
const
|
|
177
|
+
const base = aliasBaseResolver(cwd)(lib);
|
|
178
|
+
const candidates = [resolve(cwd, base, "jixoai.css"), resolve(cwd, `${base}.css`)];
|
|
102
179
|
for (const candidate of candidates) {
|
|
103
180
|
if (existsSync(candidate)) return candidate;
|
|
104
181
|
}
|
|
@@ -201,6 +278,147 @@ async function fetchRegistryItem(registryUrl, name) {
|
|
|
201
278
|
return json;
|
|
202
279
|
}
|
|
203
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Fetch + parse the registry INDEX — the same `/r/registry.json` the
|
|
283
|
+
* public site serves (`shadcn build` emits it from registry.json with
|
|
284
|
+
* `meta.group` intact, so group membership survives the pipeline).
|
|
285
|
+
* Mirrors fetchRegistryItem's URL building: the `{name}` template
|
|
286
|
+
* becomes the literal `registry`, so `https://ui.jixoai.com/r/{name}.json`
|
|
287
|
+
* resolves `https://ui.jixoai.com/r/registry.json` and local/file://
|
|
288
|
+
* mirrors land on their own index the same way.
|
|
289
|
+
*/
|
|
290
|
+
async function fetchRegistryIndex(registryUrl) {
|
|
291
|
+
const url = registryUrl.replace("{name}", "registry");
|
|
292
|
+
let raw;
|
|
293
|
+
try {
|
|
294
|
+
raw = await fetchText(url);
|
|
295
|
+
} catch (cause) {
|
|
296
|
+
throw new Error(`cannot fetch the registry index from ${url}: ${cause.message}`);
|
|
297
|
+
}
|
|
298
|
+
let json;
|
|
299
|
+
try {
|
|
300
|
+
json = JSON.parse(raw);
|
|
301
|
+
} catch {
|
|
302
|
+
throw new Error(`the registry index (${url}) is not valid JSON`);
|
|
303
|
+
}
|
|
304
|
+
const items = Array.isArray(json) ? json : json.items;
|
|
305
|
+
if (!Array.isArray(items) || items.some((i) => typeof i?.name !== "string")) {
|
|
306
|
+
throw new Error(`the registry index (${url}) has no usable items array`);
|
|
307
|
+
}
|
|
308
|
+
return items;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Resolve `add` arguments to registry item names (effect-attachments
|
|
313
|
+
* Lane H, the r5 Owner request #2, 2026-09-10 — `npx jixoai-ui add
|
|
314
|
+
* effects` / `npx jixoai-ui add effects/glass`):
|
|
315
|
+
*
|
|
316
|
+
* effects GROUP alias — expands to EVERY registry:ui item
|
|
317
|
+
* whose `meta.group === 'effects'`, in REGISTRY
|
|
318
|
+
* ORDER (the index's item order). Groups with zero
|
|
319
|
+
* registry:ui members (e.g. `engines` — lib-only)
|
|
320
|
+
* are not add-able ids.
|
|
321
|
+
* effects/glass SCOPED member — resolves to `glass` after proving
|
|
322
|
+
* the item EXISTS and its `meta.group` is exactly
|
|
323
|
+
* `effects` (a violation names both groups).
|
|
324
|
+
* glass ITEM name — as-is, any registry type. PRECEDENCE
|
|
325
|
+
* LAW: an exact item name ALWAYS wins over a group
|
|
326
|
+
* id when the two collide (none collide today; the
|
|
327
|
+
* law is fixed here so a future `effects` ITEM
|
|
328
|
+
* simply shadows the group instead of changing the
|
|
329
|
+
* resolution rules).
|
|
330
|
+
*
|
|
331
|
+
* `adopt`/`upgrade` stay ITEM-NAME-ONLY on purpose: groups are an
|
|
332
|
+
* ADD-time convenience, and the lock + the upgrade loop record items,
|
|
333
|
+
* never group ids.
|
|
334
|
+
*
|
|
335
|
+
* Degradation: a registry without an index (single-item file://
|
|
336
|
+
* fixtures) keeps the standing bare-name behavior — the index is
|
|
337
|
+
* fetched ONCE per call, and a failed fetch downgrades to a warning
|
|
338
|
+
* when every arg is a plain name, while the scoped form hard-fails
|
|
339
|
+
* (it cannot be validated without the index).
|
|
340
|
+
*/
|
|
341
|
+
export async function resolveAddNames(registryUrl, args) {
|
|
342
|
+
let index = null;
|
|
343
|
+
let indexError = null;
|
|
344
|
+
try {
|
|
345
|
+
index = await fetchRegistryIndex(registryUrl);
|
|
346
|
+
} catch (cause) {
|
|
347
|
+
indexError = cause.message;
|
|
348
|
+
}
|
|
349
|
+
if (indexError && args.some((a) => a.includes("/"))) {
|
|
350
|
+
fail(indexError);
|
|
351
|
+
}
|
|
352
|
+
if (indexError) {
|
|
353
|
+
console.warn(
|
|
354
|
+
`jixoai-ui: ${indexError} — treating every argument as an item name (group aliases need the registry index)`,
|
|
355
|
+
);
|
|
356
|
+
return [...new Set(args)];
|
|
357
|
+
}
|
|
358
|
+
const byName = new Map(index.map((i) => [i.name, i]));
|
|
359
|
+
const groups = new Map(); // group id → registry:ui member names, index order
|
|
360
|
+
for (const item of index) {
|
|
361
|
+
if (item.type !== "registry:ui") continue;
|
|
362
|
+
const group = item.meta?.group;
|
|
363
|
+
if (typeof group !== "string") continue;
|
|
364
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
365
|
+
groups.get(group).push(item.name);
|
|
366
|
+
}
|
|
367
|
+
const knownGroups = () => [...groups.keys()].join(", ");
|
|
368
|
+
|
|
369
|
+
const resolved = [];
|
|
370
|
+
const seen = new Set(); // `add effects glass` installs glass once
|
|
371
|
+
const push = (name) => {
|
|
372
|
+
if (!seen.has(name)) {
|
|
373
|
+
seen.add(name);
|
|
374
|
+
resolved.push(name);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
for (const arg of args) {
|
|
379
|
+
if (!arg.includes("/")) {
|
|
380
|
+
// bare form: an exact ITEM name always wins (see the precedence law)
|
|
381
|
+
if (byName.has(arg)) {
|
|
382
|
+
push(arg);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const members = groups.get(arg);
|
|
386
|
+
if (members) {
|
|
387
|
+
console.log(`jixoai-ui: ${arg} → ${members.join(", ")}`);
|
|
388
|
+
for (const name of members) push(name);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
fail(
|
|
392
|
+
`unknown item or group \`${arg}\` — known groups: ${knownGroups()}. ` +
|
|
393
|
+
`Pick an item from the registry index or a group id above`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
// scoped form: exactly one slash, both sides non-empty
|
|
397
|
+
const parts = arg.split("/");
|
|
398
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
399
|
+
fail(`\`${arg}\` is not a valid scoped item — expected \`group/name\` (e.g. \`effects/glass\`)`);
|
|
400
|
+
}
|
|
401
|
+
const [group, name] = parts;
|
|
402
|
+
if (!byName.has(name)) {
|
|
403
|
+
const members = groups.get(group);
|
|
404
|
+
fail(
|
|
405
|
+
members
|
|
406
|
+
? `\`${arg}\`: no registry item named \`${name}\` — group \`${group}\` has: ${members.join(", ")}`
|
|
407
|
+
: `\`${arg}\`: no registry item named \`${name}\` and \`${group}\` is not a known group — known groups: ${knownGroups()}`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const realGroup = byName.get(name).meta?.group;
|
|
411
|
+
if (realGroup !== group) {
|
|
412
|
+
fail(
|
|
413
|
+
`\`${arg}\`: \`${name}\` is not in group \`${group}\` — it belongs to ` +
|
|
414
|
+
`\`${realGroup ?? "(no group)"}\`. Use \`npx jixoai-ui add ${name}\` instead`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
push(name);
|
|
418
|
+
}
|
|
419
|
+
return resolved;
|
|
420
|
+
}
|
|
421
|
+
|
|
204
422
|
function assertRegistryFiles(name, files) {
|
|
205
423
|
for (const file of files) {
|
|
206
424
|
if (typeof file.target !== "string" || typeof file.content !== "string") {
|
|
@@ -212,13 +430,15 @@ function assertRegistryFiles(name, files) {
|
|
|
212
430
|
function resolveInstallPath(target, config, cwd) {
|
|
213
431
|
// registry targets are alias-relative ("@ui/toc.svelte" → aliases.ui) or
|
|
214
432
|
// plain project-relative paths; mirrors how shadcn places registry files.
|
|
433
|
+
// `$`-rooted alias values resolve through the project's tsconfig paths
|
|
434
|
+
// (aliasDir) — see the $-alias resolution block above.
|
|
215
435
|
const match = /^@([\w.$-]+)(?:\/(.+))?$/.exec(target);
|
|
216
436
|
if (match) {
|
|
217
437
|
const base = config.aliases?.[match[1]];
|
|
218
438
|
if (typeof base !== "string") {
|
|
219
439
|
throw new Error(`cannot place \`${target}\`: components.json has no aliases.${match[1]}`);
|
|
220
440
|
}
|
|
221
|
-
return resolve(
|
|
441
|
+
return resolve(aliasDir(base, cwd), match[2] ?? "");
|
|
222
442
|
}
|
|
223
443
|
return resolve(cwd, target);
|
|
224
444
|
}
|
|
@@ -262,6 +482,15 @@ function writeLock(path, lock) {
|
|
|
262
482
|
* Record freshly installed items in the lock. Shared by add/init; upgrade
|
|
263
483
|
* reuses the same fetch/hash/place helpers. The install itself already
|
|
264
484
|
* succeeded, so a recording failure warns instead of failing the command.
|
|
485
|
+
*
|
|
486
|
+
* Install-integrity gate (consumer-feedback-fixes P0-3, 2026-09-06):
|
|
487
|
+
* an item is locked ONLY when every one of its files exists on disk at
|
|
488
|
+
* the alias-resolved install path. A non-interactive shadcn run whose
|
|
489
|
+
* overwrite confirmation hits EOF cancels its whole write phase — the
|
|
490
|
+
* files never land, and the pre-gate CLI locked the item anyway, so
|
|
491
|
+
* `upgrade` reported it as managed while nothing was installed. A miss
|
|
492
|
+
* now keeps the item OUT of the lock and prints the missing paths with
|
|
493
|
+
* the recovery guidance.
|
|
265
494
|
*/
|
|
266
495
|
async function recordInstalledItems(cwd, config, names) {
|
|
267
496
|
const registryUrl = registryUrlFor(config);
|
|
@@ -272,8 +501,27 @@ async function recordInstalledItems(cwd, config, names) {
|
|
|
272
501
|
const item = await fetchRegistryItem(registryUrl, name);
|
|
273
502
|
assertRegistryFiles(name, item.files);
|
|
274
503
|
const files = {};
|
|
504
|
+
const missing = [];
|
|
275
505
|
for (const file of item.files) {
|
|
276
|
-
|
|
506
|
+
const key = lockInstallKey(file.target, config, cwd);
|
|
507
|
+
if (!existsSync(resolve(cwd, key))) {
|
|
508
|
+
missing.push(key);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
files[key] = sha256(file.content);
|
|
512
|
+
}
|
|
513
|
+
if (missing.length > 0) {
|
|
514
|
+
console.warn(
|
|
515
|
+
`jixoai-ui: ${name} NOT locked in ${LOCK_NAME} — ${missing.length} of ` +
|
|
516
|
+
`${item.files.length} file(s) missing at their install path(s):`,
|
|
517
|
+
);
|
|
518
|
+
for (const key of missing) console.warn(` - ${key}`);
|
|
519
|
+
console.warn(
|
|
520
|
+
"jixoai-ui: the write phase was cancelled (shadcn's overwrite confirmation " +
|
|
521
|
+
"hit EOF under non-interactive stdin). Move the conflicting existing file(s) " +
|
|
522
|
+
"aside and re-run the add; this CLI re-applies the brand hue afterwards.",
|
|
523
|
+
);
|
|
524
|
+
continue;
|
|
277
525
|
}
|
|
278
526
|
lock.items[name] = { ...(lock.items[name] ?? {}), files };
|
|
279
527
|
recorded++;
|
|
@@ -289,6 +537,64 @@ async function recordInstalledItems(cwd, config, names) {
|
|
|
289
537
|
}
|
|
290
538
|
}
|
|
291
539
|
|
|
540
|
+
/* ── post-add relocation (consumer-feedback-fixes P0-3) ── */
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* shadcn sometimes ignores the registry item's alias targets and drops
|
|
544
|
+
* files into LITERAL `src/@lib/`, `src/@ui/` and `src/vite-plugins/`
|
|
545
|
+
* directories (the alias prefix treated as a path segment). This pass
|
|
546
|
+
* walks those literal directories, moves every dropped file to its
|
|
547
|
+
* alias-resolved (or project-root) destination, reports each move, and
|
|
548
|
+
* removes the emptied literal directories. Destination collisions are
|
|
549
|
+
* reported, never clobbered.
|
|
550
|
+
*/
|
|
551
|
+
function relocateMisplacedFiles(cwd, config) {
|
|
552
|
+
const alias = (name) => {
|
|
553
|
+
const base = config.aliases?.[name];
|
|
554
|
+
if (typeof base !== "string") return null;
|
|
555
|
+
return aliasDir(base, cwd); // $-rooted values resolve through tsconfig paths
|
|
556
|
+
};
|
|
557
|
+
const sources = [
|
|
558
|
+
{ dir: resolve(cwd, "src/@lib"), destination: alias("lib") },
|
|
559
|
+
{ dir: resolve(cwd, "src/@ui"), destination: alias("ui") },
|
|
560
|
+
{ dir: resolve(cwd, "src/@components"), destination: alias("components") },
|
|
561
|
+
// plain project-relative targets (e.g. vite-plugins/llms-txt.mjs)
|
|
562
|
+
// that shadcn still anchors under src/
|
|
563
|
+
{ dir: resolve(cwd, "src/vite-plugins"), destination: resolve(cwd, "vite-plugins") },
|
|
564
|
+
];
|
|
565
|
+
for (const { dir, destination } of sources) {
|
|
566
|
+
if (!destination || !existsSync(dir)) continue;
|
|
567
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
568
|
+
if (entries.length === 0) {
|
|
569
|
+
rmdirSync(dir);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const moveTree = (fromDir, toDir) => {
|
|
573
|
+
mkdirSync(toDir, { recursive: true });
|
|
574
|
+
for (const entry of readdirSync(fromDir, { withFileTypes: true })) {
|
|
575
|
+
const from = join(fromDir, entry.name);
|
|
576
|
+
const to = join(toDir, entry.name);
|
|
577
|
+
if (entry.isDirectory()) {
|
|
578
|
+
moveTree(from, to);
|
|
579
|
+
rmdirSync(from);
|
|
580
|
+
} else if (existsSync(to)) {
|
|
581
|
+
console.warn(
|
|
582
|
+
`jixoai-ui: relocation skipped — ${toPosix(relative(cwd, to))} already exists; ` +
|
|
583
|
+
`${toPosix(relative(cwd, from))} left in place`,
|
|
584
|
+
);
|
|
585
|
+
} else {
|
|
586
|
+
renameSync(from, to);
|
|
587
|
+
console.log(
|
|
588
|
+
`jixoai-ui: relocated ${toPosix(relative(cwd, from))} → ${toPosix(relative(cwd, to))}`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
moveTree(dir, destination);
|
|
594
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) rmdirSync(dir);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
292
598
|
/* ── upgrade ── */
|
|
293
599
|
|
|
294
600
|
function appCssCandidates(cwd, config, lock) {
|
|
@@ -407,18 +713,19 @@ const cwd = process.cwd();
|
|
|
407
713
|
switch (command) {
|
|
408
714
|
case "init": {
|
|
409
715
|
const { path, config } = readConfig(cwd);
|
|
410
|
-
const hue = hueFromArgs(rest, config.jixoai?.brandHue ?? DEFAULT_HUE);
|
|
716
|
+
const hue = hueFromArgs(rest.filter((a) => !a.startsWith("--")), config.jixoai?.brandHue ?? DEFAULT_HUE);
|
|
411
717
|
ensureNamespace(config);
|
|
412
718
|
config.jixoai = { ...(config.jixoai ?? {}), brandHue: hue };
|
|
413
719
|
writeConfig(path, config);
|
|
414
720
|
console.log(`jixoai-ui: ${NAMESPACE} namespace + jixoai config written → ${path}`);
|
|
415
721
|
shadcn(["add", `${NAMESPACE}/${THEME_ITEM}`], cwd, path, config);
|
|
722
|
+
relocateMisplacedFiles(cwd, readConfig(cwd).config);
|
|
416
723
|
applyHue(themeCssPath(config, cwd), hue);
|
|
417
724
|
await recordInstalledItems(cwd, readConfig(cwd).config, [THEME_ITEM]);
|
|
418
725
|
break;
|
|
419
726
|
}
|
|
420
727
|
case "hue": {
|
|
421
|
-
const hue = hueFromArgs(["--hue", rest
|
|
728
|
+
const hue = hueFromArgs(["--hue", rest.find((a) => !a.startsWith("--"))]);
|
|
422
729
|
const { path, config } = readConfig(cwd);
|
|
423
730
|
config.jixoai = { ...(config.jixoai ?? {}), brandHue: hue };
|
|
424
731
|
writeConfig(path, config);
|
|
@@ -426,14 +733,35 @@ switch (command) {
|
|
|
426
733
|
break;
|
|
427
734
|
}
|
|
428
735
|
case "add": {
|
|
429
|
-
|
|
736
|
+
// arg discipline (consumer-feedback-fixes P0-3): `--` tokens are
|
|
737
|
+
// flags, never item names (`add --help` used to spawn
|
|
738
|
+
// `shadcn add @jixoai/--help`)
|
|
739
|
+
if (rest.some((a) => a === "--help" || a === "-h")) {
|
|
740
|
+
console.log(USAGE);
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
const items = rest.filter((a) => !a.startsWith("--"));
|
|
744
|
+
if (items.length === 0) {
|
|
745
|
+
fail("add needs at least one item name (e.g. `toc`, a group id like `effects`, or `effects/glass`)");
|
|
746
|
+
}
|
|
430
747
|
const { path, config } = readConfig(cwd);
|
|
748
|
+
// group aliases (effect-attachments Lane H): `add effects` /
|
|
749
|
+
// `add effects/glass` resolve to ITEM names BEFORE the shadcn
|
|
750
|
+
// loop — the loop, the lock and the recording all speak RESOLVED
|
|
751
|
+
// names (so `add effects` locks glass + press-button, never an
|
|
752
|
+
// `effects` key)
|
|
753
|
+
const resolved = await resolveAddNames(registryUrlFor(config), items);
|
|
431
754
|
const hue = config.jixoai?.brandHue ?? DEFAULT_HUE;
|
|
432
|
-
|
|
755
|
+
// one shadcn invocation PER ITEM, each carrying the @jixoai/ prefix
|
|
756
|
+
// itself (consumer-feedback-fixes P0-3 audit: the prefix must never
|
|
757
|
+
// depend on shell/shadcn multi-arg behavior — the loop re-reads the
|
|
758
|
+
// config because shadcn may rewrite it between spawns)
|
|
759
|
+
for (const item of resolved) {
|
|
433
760
|
shadcn(["add", `${NAMESPACE}/${item}`], cwd, path, readConfig(cwd).config);
|
|
434
761
|
}
|
|
762
|
+
relocateMisplacedFiles(cwd, readConfig(cwd).config);
|
|
435
763
|
applyHue(themeCssPath(config, cwd), hue);
|
|
436
|
-
await recordInstalledItems(cwd, readConfig(cwd).config,
|
|
764
|
+
await recordInstalledItems(cwd, readConfig(cwd).config, resolved);
|
|
437
765
|
break;
|
|
438
766
|
}
|
|
439
767
|
case "adopt": {
|
|
@@ -441,7 +769,9 @@ switch (command) {
|
|
|
441
769
|
// baselines the CURRENT disk content of the named items into the lock:
|
|
442
770
|
// the first `upgrade` afterwards diffs registry canon against this
|
|
443
771
|
// baseline, applies changes + hue, and the lock flips to canonical
|
|
444
|
-
// hashes — subsequent upgrades are fully idempotent.
|
|
772
|
+
// hashes — subsequent upgrades are fully idempotent. Item names only:
|
|
773
|
+
// group aliases (`effects`, `effects/glass`) are an ADD-time
|
|
774
|
+
// convenience and never expand here (see resolveAddNames).
|
|
445
775
|
const names = rest.filter((a) => !a.startsWith("--"));
|
|
446
776
|
if (names.length === 0) {
|
|
447
777
|
fail("adopt needs item names (e.g. `adopt toc jixoai-theme`) — items whose files live at their components.json targets");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jixoai-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Official jixoai design-language CLI: initializes the @jixoai shadcn registry namespace, manages the jixoai extension fields in components.json, applies the per-project brand hue, and performs locked idempotent upgrades.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"url": "https://github.com/jixoai/ui",
|
|
30
30
|
"directory": "cli"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|