guford-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.mjs +359 -0
- package/package.json +16 -0
package/bin.mjs
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* guford-ui — CLI maison (façon shadcn) pour installer des composants/utils
|
|
5
|
+
* en copiant leur code source dans un projet Angular cible.
|
|
6
|
+
*
|
|
7
|
+
* Le registry peut être lu :
|
|
8
|
+
* - en LOCAL (quand on exécute la CLI depuis le dépôt guford-ui), ou
|
|
9
|
+
* - à DISTANCE via une URL (fetch) — pour `npx guford-ui ...` depuis n'importe quel projet.
|
|
10
|
+
*
|
|
11
|
+
* Commandes :
|
|
12
|
+
* guford-ui init Installe le theming (tokens + preset Tailwind) + config projet
|
|
13
|
+
* guford-ui list Liste les items disponibles
|
|
14
|
+
* guford-ui info <name> Détaille un item (fichiers + dépendances)
|
|
15
|
+
* guford-ui add <name...> Copie un/des item(s) (+ ses registryDependencies) dans le projet
|
|
16
|
+
*
|
|
17
|
+
* Options communes :
|
|
18
|
+
* --cwd <path> Projet cible (défaut: répertoire courant)
|
|
19
|
+
* --dir <path> Racine d'installation dans le projet (défaut: guford-ui.json puis "src")
|
|
20
|
+
* --registry <url> Base distante du registry (sinon $GUFORD_UI_REGISTRY, sinon local, sinon défaut)
|
|
21
|
+
* --overwrite Écrase les fichiers existants sans demander
|
|
22
|
+
* --dry-run Affiche ce qui serait fait sans rien écrire
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
26
|
+
import { dirname, basename, join, resolve, relative } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const ROOT = resolve(__dirname, '..'); // racine du dépôt guford-ui (mode local)
|
|
31
|
+
const CONFIG_FILE = 'guford-ui.json';
|
|
32
|
+
|
|
33
|
+
// Base distante par défaut (à adapter après publication : GitHub raw, host statique…).
|
|
34
|
+
const DEFAULT_REMOTE = 'https://raw.githubusercontent.com/bekmarc/guford-ui/main';
|
|
35
|
+
|
|
36
|
+
// --- couleurs ANSI ---------------------------------------------------------
|
|
37
|
+
const c = {
|
|
38
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
39
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
40
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
41
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
42
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
43
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function fail(msg) {
|
|
47
|
+
console.error(c.red(`✖ ${msg}`));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- source du registry (local FS ou URL distante) -------------------------
|
|
52
|
+
/**
|
|
53
|
+
* @param {{registry?: string}} flags
|
|
54
|
+
* @returns {{remote: boolean, base: string}}
|
|
55
|
+
*/
|
|
56
|
+
function resolveSource(flags) {
|
|
57
|
+
const explicit = flags.registry || process.env.GUFORD_UI_REGISTRY;
|
|
58
|
+
if (explicit) return { remote: true, base: explicit.replace(/\/+$/, '') };
|
|
59
|
+
if (existsSync(join(ROOT, 'registry.json'))) return { remote: false, base: ROOT };
|
|
60
|
+
return { remote: true, base: DEFAULT_REMOTE };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Lit un fichier texte du registry (chemin relatif à la racine, ex: 'registry.json'). */
|
|
64
|
+
async function readText(source, rel) {
|
|
65
|
+
if (source.remote) {
|
|
66
|
+
const url = `${source.base}/${rel}`;
|
|
67
|
+
const res = await fetch(url);
|
|
68
|
+
if (!res.ok) fail(`Téléchargement échoué (${res.status}) : ${url}`);
|
|
69
|
+
return await res.text();
|
|
70
|
+
}
|
|
71
|
+
const path = join(source.base, rel);
|
|
72
|
+
if (!existsSync(path)) fail(`Fichier introuvable: ${path}`);
|
|
73
|
+
return readFileSync(path, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Lit un fichier (texte ou binaire) du registry → Buffer. */
|
|
77
|
+
async function readBuffer(source, rel) {
|
|
78
|
+
if (source.remote) {
|
|
79
|
+
const url = `${source.base}/${rel}`;
|
|
80
|
+
const res = await fetch(url);
|
|
81
|
+
if (!res.ok) fail(`Téléchargement échoué (${res.status}) : ${url}`);
|
|
82
|
+
return Buffer.from(await res.arrayBuffer());
|
|
83
|
+
}
|
|
84
|
+
const path = join(source.base, rel);
|
|
85
|
+
if (!existsSync(path)) fail(`Fichier introuvable: ${path}`);
|
|
86
|
+
return readFileSync(path);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function loadRegistry(source) {
|
|
90
|
+
/** @type {{ name: string, items: any[], styles?: { files: string[] } }} */
|
|
91
|
+
const reg = JSON.parse(await readText(source, 'registry.json'));
|
|
92
|
+
return reg;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findItem(reg, name) {
|
|
96
|
+
return reg.items.find((it) => it.name === name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Résout récursivement un item + ses registryDependencies (deps avant l'item). */
|
|
100
|
+
function resolveWithDeps(reg, name, acc = new Map()) {
|
|
101
|
+
if (acc.has(name)) return acc;
|
|
102
|
+
const item = findItem(reg, name);
|
|
103
|
+
if (!item) fail(`Item inconnu: "${name}". Lance "guford-ui list" pour voir les items dispo.`);
|
|
104
|
+
for (const dep of item.registryDependencies ?? []) resolveWithDeps(reg, dep, acc);
|
|
105
|
+
acc.set(name, item);
|
|
106
|
+
return acc;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Config projet guford-ui.json (toujours dans le projet cible local). */
|
|
110
|
+
function loadProjectConfig(cwd) {
|
|
111
|
+
const path = resolve(process.cwd(), cwd, CONFIG_FILE);
|
|
112
|
+
if (existsSync(path)) {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
115
|
+
} catch {
|
|
116
|
+
fail(`${CONFIG_FILE} illisible (JSON invalide): ${path}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- parseur d'arguments ---------------------------------------------------
|
|
123
|
+
function parseArgs(argv) {
|
|
124
|
+
const positionals = [];
|
|
125
|
+
const flags = { cwd: '.', dir: undefined, overwrite: false, dryRun: false, registry: undefined };
|
|
126
|
+
for (let i = 0; i < argv.length; i++) {
|
|
127
|
+
const a = argv[i];
|
|
128
|
+
if (a === '--overwrite') flags.overwrite = true;
|
|
129
|
+
else if (a === '--dry-run') flags.dryRun = true;
|
|
130
|
+
else if (a === '--cwd') flags.cwd = argv[++i];
|
|
131
|
+
else if (a === '--dir') flags.dir = argv[++i];
|
|
132
|
+
else if (a === '--registry') flags.registry = argv[++i];
|
|
133
|
+
else if (a.startsWith('--')) fail(`Option inconnue: ${a}`);
|
|
134
|
+
else positionals.push(a);
|
|
135
|
+
}
|
|
136
|
+
return { positionals, flags };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sourceLabel(source) {
|
|
140
|
+
return source.remote ? c.dim(`(registry distant: ${source.base})`) : c.dim('(registry local)');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- commandes -------------------------------------------------------------
|
|
144
|
+
async function cmdList(flags) {
|
|
145
|
+
const source = resolveSource(flags);
|
|
146
|
+
const reg = await loadRegistry(source);
|
|
147
|
+
console.log(c.bold(`\n ${reg.name} — ${reg.items.length} items disponibles `) + sourceLabel(source) + '\n');
|
|
148
|
+
for (const it of reg.items) {
|
|
149
|
+
const tag = it.type.replace('registry:', '');
|
|
150
|
+
console.log(` ${c.green(it.name.padEnd(18))} ${c.dim('[' + tag + ']')}`);
|
|
151
|
+
console.log(` ${' '.repeat(18)} ${c.dim(it.description ?? '')}\n`);
|
|
152
|
+
}
|
|
153
|
+
console.log(c.dim(` → guford-ui add <name>\n`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function cmdInfo(name, flags) {
|
|
157
|
+
if (!name) fail('Usage: guford-ui info <name>');
|
|
158
|
+
const source = resolveSource(flags);
|
|
159
|
+
const reg = await loadRegistry(source);
|
|
160
|
+
const it = findItem(reg, name);
|
|
161
|
+
if (!it) fail(`Item inconnu: "${name}"`);
|
|
162
|
+
console.log(c.bold(`\n ${it.name} ${c.dim('[' + it.type + ']')}`));
|
|
163
|
+
console.log(` ${it.description ?? ''}\n`);
|
|
164
|
+
console.log(c.cyan(' Fichiers:'));
|
|
165
|
+
for (const f of it.files) console.log(` ${f.path} ${c.dim('→ ' + f.target)}`);
|
|
166
|
+
console.log(c.cyan('\n Dépendances npm:'));
|
|
167
|
+
console.log(` ${(it.dependencies ?? []).join(', ') || c.dim('aucune')}`);
|
|
168
|
+
console.log(c.cyan('\n Dépendances registry:'));
|
|
169
|
+
console.log(` ${(it.registryDependencies ?? []).join(', ') || c.dim('aucune')}\n`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Écrit un fichier sur disque avec gestion skip/overwrite/dry-run. Retourne 'written'|'skipped'. */
|
|
173
|
+
function writeFileSafe(dest, buffer, flags, log = []) {
|
|
174
|
+
const exists = existsSync(dest);
|
|
175
|
+
if (exists && !flags.overwrite) {
|
|
176
|
+
log.push(` ${c.yellow('~ skip')} ${relative(process.cwd(), dest)} ${c.dim('(--overwrite pour forcer)')}`);
|
|
177
|
+
return 'skipped';
|
|
178
|
+
}
|
|
179
|
+
if (!flags.dryRun) {
|
|
180
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
181
|
+
writeFileSync(dest, buffer);
|
|
182
|
+
}
|
|
183
|
+
log.push(` ${c.green(exists ? '↻ over' : '+ add ')} ${relative(process.cwd(), dest)}`);
|
|
184
|
+
return 'written';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function cmdAdd(names, flags) {
|
|
188
|
+
if (!names.length)
|
|
189
|
+
fail('Usage: guford-ui add <name...> [--cwd .] [--dir src] [--registry <url>] [--overwrite] [--dry-run]');
|
|
190
|
+
const source = resolveSource(flags);
|
|
191
|
+
const reg = await loadRegistry(source);
|
|
192
|
+
|
|
193
|
+
const resolved = new Map();
|
|
194
|
+
for (const n of names) resolveWithDeps(reg, n, resolved);
|
|
195
|
+
|
|
196
|
+
const config = loadProjectConfig(flags.cwd);
|
|
197
|
+
const dir = flags.dir ?? config?.dir ?? 'src';
|
|
198
|
+
const targetRoot = resolve(process.cwd(), flags.cwd, dir);
|
|
199
|
+
|
|
200
|
+
console.log(
|
|
201
|
+
c.bold(`\n Installation de ${[...resolved.keys()].map(c.green).join(', ')}`) +
|
|
202
|
+
(flags.dryRun ? c.yellow(' (dry-run)') : '') +
|
|
203
|
+
' ' +
|
|
204
|
+
sourceLabel(source),
|
|
205
|
+
);
|
|
206
|
+
if (config) console.log(c.dim(` config ${CONFIG_FILE} détectée (dir="${dir}")`));
|
|
207
|
+
console.log(c.dim(` cible: ${targetRoot}\n`));
|
|
208
|
+
|
|
209
|
+
const npmDeps = new Set();
|
|
210
|
+
const log = [];
|
|
211
|
+
let written = 0;
|
|
212
|
+
let skipped = 0;
|
|
213
|
+
|
|
214
|
+
for (const item of resolved.values()) {
|
|
215
|
+
for (const dep of item.dependencies ?? []) npmDeps.add(dep);
|
|
216
|
+
for (const file of item.files) {
|
|
217
|
+
const buffer = await readBuffer(source, `registry/${file.path}`);
|
|
218
|
+
const dest = join(targetRoot, file.target);
|
|
219
|
+
const r = writeFileSafe(dest, buffer, flags, log);
|
|
220
|
+
r === 'written' ? written++ : skipped++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(log.join('\n'));
|
|
225
|
+
console.log('');
|
|
226
|
+
if (npmDeps.size) {
|
|
227
|
+
console.log(c.cyan(' Dépendances npm à installer dans le projet cible:'));
|
|
228
|
+
console.log(` ${c.bold('npm install ' + [...npmDeps].join(' '))}\n`);
|
|
229
|
+
}
|
|
230
|
+
console.log(
|
|
231
|
+
c.dim(
|
|
232
|
+
` ${written} fichier(s) ${flags.dryRun ? 'seraient écrits' : 'écrits'}` +
|
|
233
|
+
(skipped ? `, ${skipped} ignoré(s)` : '') +
|
|
234
|
+
'.\n',
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function cmdInit(flags) {
|
|
240
|
+
const source = resolveSource(flags);
|
|
241
|
+
const reg = await loadRegistry(source);
|
|
242
|
+
const dir = flags.dir ?? 'src';
|
|
243
|
+
const projectRoot = resolve(process.cwd(), flags.cwd);
|
|
244
|
+
const stylesDest = join(projectRoot, dir, 'styles');
|
|
245
|
+
|
|
246
|
+
console.log(
|
|
247
|
+
c.bold('\n Initialisation du theming guford-ui') +
|
|
248
|
+
(flags.dryRun ? c.yellow(' (dry-run)') : '') +
|
|
249
|
+
' ' +
|
|
250
|
+
sourceLabel(source),
|
|
251
|
+
);
|
|
252
|
+
console.log(c.dim(` projet: ${projectRoot}\n`));
|
|
253
|
+
|
|
254
|
+
const styleFiles = reg.styles?.files ?? [
|
|
255
|
+
'styles/tokens.css',
|
|
256
|
+
'styles/tailwind.preset.cjs',
|
|
257
|
+
'styles/inter/Inter-roman.var.woff2',
|
|
258
|
+
'styles/inter/Inter-italic.var.woff2',
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const log = [];
|
|
262
|
+
let written = 0;
|
|
263
|
+
let skipped = 0;
|
|
264
|
+
|
|
265
|
+
// Mappe chaque fichier de theming vers son emplacement dans le projet cible.
|
|
266
|
+
for (const sf of styleFiles) {
|
|
267
|
+
const name = basename(sf);
|
|
268
|
+
let dest;
|
|
269
|
+
if (name === 'tokens.css') dest = join(stylesDest, 'guford-ui-tokens.css');
|
|
270
|
+
else if (name.endsWith('.cjs')) dest = join(projectRoot, name); // preset à la racine
|
|
271
|
+
else if (sf.includes('/inter/')) dest = join(stylesDest, 'inter', name);
|
|
272
|
+
else dest = join(stylesDest, name);
|
|
273
|
+
|
|
274
|
+
const buffer = await readBuffer(source, `registry/${sf}`);
|
|
275
|
+
const r = writeFileSafe(dest, buffer, flags, log);
|
|
276
|
+
r === 'written' ? written++ : skipped++;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// guford-ui.json (config projet)
|
|
280
|
+
const configDest = join(projectRoot, CONFIG_FILE);
|
|
281
|
+
if (existsSync(configDest) && !flags.overwrite) {
|
|
282
|
+
skipped++;
|
|
283
|
+
log.push(` ${c.yellow('~ skip')} ${relative(process.cwd(), configDest)} ${c.dim('(existe déjà)')}`);
|
|
284
|
+
} else {
|
|
285
|
+
const cfg = {
|
|
286
|
+
$schema: 'https://guford-ui/schema.json',
|
|
287
|
+
dir,
|
|
288
|
+
aliases: { components: 'components', lib: 'lib', directives: 'directives', utils: 'lib' },
|
|
289
|
+
};
|
|
290
|
+
if (!flags.dryRun) writeFileSync(configDest, JSON.stringify(cfg, null, 2) + '\n');
|
|
291
|
+
written++;
|
|
292
|
+
log.push(` ${c.green('+ add ')} ${relative(process.cwd(), configDest)}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log(log.join('\n'));
|
|
296
|
+
console.log(c.cyan('\n Étapes de câblage:'));
|
|
297
|
+
console.log(` 1. Styles globaux — ajoutez: ${c.bold(`@import './styles/guford-ui-tokens.css';`)}`);
|
|
298
|
+
console.log(` ${c.dim(`(dans ${dir}/styles.scss ou ${dir}/styles.css)`)}`);
|
|
299
|
+
console.log(` 2. tailwind.config.js — ajoutez: ${c.bold(`presets: [require('./tailwind.preset.cjs')]`)}`);
|
|
300
|
+
console.log(` 3. Peer-deps de base: ${c.bold('npm install clsx tailwind-merge')}`);
|
|
301
|
+
console.log(` ${c.dim('(certains composants requièrent aussi @coreui/angular — voir `info <name>`)')}`);
|
|
302
|
+
console.log(
|
|
303
|
+
c.dim(
|
|
304
|
+
`\n ${written} fichier(s) ${flags.dryRun ? 'seraient écrits' : 'écrits'}` +
|
|
305
|
+
(skipped ? `, ${skipped} ignoré(s)` : '') +
|
|
306
|
+
`. Ensuite: guford-ui add <composant>\n`,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function help() {
|
|
312
|
+
console.log(`
|
|
313
|
+
${c.bold('guford-ui')} — registry de composants Angular (façon shadcn)
|
|
314
|
+
|
|
315
|
+
${c.cyan('Commandes:')}
|
|
316
|
+
init Installe le theming (tokens + preset) + config projet
|
|
317
|
+
list Liste les items disponibles
|
|
318
|
+
info <name> Détaille un item
|
|
319
|
+
add <name...> Copie un/des item(s) dans le projet cible
|
|
320
|
+
|
|
321
|
+
${c.cyan('Options:')}
|
|
322
|
+
--cwd <path> Projet cible (défaut: .)
|
|
323
|
+
--dir <path> Racine d'installation (défaut: guford-ui.json puis src)
|
|
324
|
+
--registry <url> Base distante du registry (sinon $GUFORD_UI_REGISTRY, sinon local)
|
|
325
|
+
--overwrite Écrase les fichiers existants
|
|
326
|
+
--dry-run Simule sans écrire
|
|
327
|
+
`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- entrée ----------------------------------------------------------------
|
|
331
|
+
async function main() {
|
|
332
|
+
const [, , command, ...rest] = process.argv;
|
|
333
|
+
const { positionals, flags } = parseArgs(rest);
|
|
334
|
+
|
|
335
|
+
switch (command) {
|
|
336
|
+
case 'init':
|
|
337
|
+
await cmdInit(flags);
|
|
338
|
+
break;
|
|
339
|
+
case 'list':
|
|
340
|
+
await cmdList(flags);
|
|
341
|
+
break;
|
|
342
|
+
case 'info':
|
|
343
|
+
await cmdInfo(positionals[0], flags);
|
|
344
|
+
break;
|
|
345
|
+
case 'add':
|
|
346
|
+
await cmdAdd(positionals, flags);
|
|
347
|
+
break;
|
|
348
|
+
case undefined:
|
|
349
|
+
case '-h':
|
|
350
|
+
case '--help':
|
|
351
|
+
case 'help':
|
|
352
|
+
help();
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
fail(`Commande inconnue: "${command}". Lance "guford-ui --help".`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main().catch((err) => fail(err?.message ?? String(err)));
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guford-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI pour installer les composants du registry guford-ui (façon shadcn).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"guford-ui": "./bin.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": ["bin.mjs"],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
}
|
|
16
|
+
}
|