guford-ui 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/bin.mjs +262 -38
  2. package/package.json +1 -1
package/bin.mjs CHANGED
@@ -25,13 +25,15 @@
25
25
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
26
26
  import { dirname, basename, join, resolve, relative } from 'node:path';
27
27
  import { fileURLToPath } from 'node:url';
28
+ import { execSync } from 'node:child_process';
28
29
 
29
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
30
31
  const ROOT = resolve(__dirname, '..'); // racine du dépôt guford-ui (mode local)
31
32
  const CONFIG_FILE = 'guford-ui.json';
32
33
 
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';
34
+ // Dépôt GitHub par défaut (privé lu via l'API Contents avec token).
35
+ const DEFAULT_REPO = 'bekmarc/guford-ui';
36
+ const DEFAULT_REF = 'main';
35
37
 
36
38
  // --- couleurs ANSI ---------------------------------------------------------
37
39
  const c = {
@@ -48,42 +50,72 @@ function fail(msg) {
48
50
  process.exit(1);
49
51
  }
50
52
 
51
- // --- source du registry (local FS ou URL distante) -------------------------
53
+ // --- source du registry (local FS | URL statique | API GitHub privée) ------
52
54
  /**
53
- * @param {{registry?: string}} flags
54
- * @returns {{remote: boolean, base: string}}
55
+ * Résout la source du registry. 3 modes :
56
+ * - 'local' : fichiers du dépôt (exécution depuis quitus-ui/guford-ui)
57
+ * - 'url' : base HTTP statique (--registry / $GUFORD_UI_REGISTRY), token facultatif
58
+ * - 'github' : dépôt GitHub privé via l'API Contents (--repo/--ref + token requis)
55
59
  */
56
60
  function resolveSource(flags) {
61
+ const token = flags.token || process.env.GUFORD_UI_TOKEN || process.env.GITHUB_TOKEN;
57
62
  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 };
63
+ if (explicit) return { kind: 'url', base: explicit.replace(/\/+$/, ''), token };
64
+ if (!flags.repo && existsSync(join(ROOT, 'registry.json'))) return { kind: 'local', base: ROOT };
65
+ return { kind: 'github', repo: flags.repo || DEFAULT_REPO, ref: flags.ref || DEFAULT_REF, token };
66
+ }
67
+
68
+ function sourceUrl(source, rel) {
69
+ if (source.kind === 'url') return `${source.base}/${rel}`;
70
+ // GitHub Contents API : renvoie le contenu brut (texte + binaire) avec Accept: raw
71
+ return `https://api.github.com/repos/${source.repo}/contents/${rel}?ref=${source.ref}`;
72
+ }
73
+
74
+ function sourceHeaders(source) {
75
+ const h = { 'User-Agent': 'guford-ui-cli' };
76
+ if (source.kind === 'github') {
77
+ if (!source.token)
78
+ fail(
79
+ 'Dépôt privé : un token est requis.\n' +
80
+ ' → crée un PAT GitHub (Contents: read) puis: set GUFORD_UI_TOKEN=<token>\n' +
81
+ ' (ou passe --token <token>, ou utilise --registry <url-publique>)',
82
+ );
83
+ h.Authorization = `Bearer ${source.token}`;
84
+ h.Accept = 'application/vnd.github.raw';
85
+ } else if (source.token) {
86
+ h.Authorization = `Bearer ${source.token}`;
87
+ }
88
+ return h;
89
+ }
90
+
91
+ async function fetchEntry(source, rel) {
92
+ const url = sourceUrl(source, rel);
93
+ const res = await fetch(url, { headers: sourceHeaders(source) });
94
+ if (!res.ok) {
95
+ const hint = res.status === 404 ? ' (introuvable, ou token sans accès au dépôt privé)' : '';
96
+ fail(`Téléchargement échoué (${res.status})${hint} : ${url}`);
97
+ }
98
+ return Buffer.from(await res.arrayBuffer());
61
99
  }
62
100
 
63
101
  /** Lit un fichier texte du registry (chemin relatif à la racine, ex: 'registry.json'). */
64
102
  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();
103
+ if (source.kind === 'local') {
104
+ const path = join(source.base, rel);
105
+ if (!existsSync(path)) fail(`Fichier introuvable: ${path}`);
106
+ return readFileSync(path, 'utf8');
70
107
  }
71
- const path = join(source.base, rel);
72
- if (!existsSync(path)) fail(`Fichier introuvable: ${path}`);
73
- return readFileSync(path, 'utf8');
108
+ return (await fetchEntry(source, rel)).toString('utf8');
74
109
  }
75
110
 
76
111
  /** Lit un fichier (texte ou binaire) du registry → Buffer. */
77
112
  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());
113
+ if (source.kind === 'local') {
114
+ const path = join(source.base, rel);
115
+ if (!existsSync(path)) fail(`Fichier introuvable: ${path}`);
116
+ return readFileSync(path);
83
117
  }
84
- const path = join(source.base, rel);
85
- if (!existsSync(path)) fail(`Fichier introuvable: ${path}`);
86
- return readFileSync(path);
118
+ return await fetchEntry(source, rel);
87
119
  }
88
120
 
89
121
  async function loadRegistry(source) {
@@ -119,17 +151,147 @@ function loadProjectConfig(cwd) {
119
151
  return null;
120
152
  }
121
153
 
154
+ // --- auto-câblage du projet cible ------------------------------------------
155
+ /** Détecte la structure du projet Angular consommateur. */
156
+ function detectProject(cwd) {
157
+ const root = resolve(process.cwd(), cwd);
158
+ const out = { root, ngMajor: null, ngJsonPath: null, ngJson: null, projName: null, sourceRoot: 'src', stylesFile: null, appConfig: null, hasTailwind: false };
159
+
160
+ const pkgPath = join(root, 'package.json');
161
+ if (existsSync(pkgPath)) {
162
+ try {
163
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
164
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
165
+ const ver = deps['@angular/core'];
166
+ const m = ver && ver.match(/(\d+)/);
167
+ if (m) out.ngMajor = Number(m[1]);
168
+ out.hasTailwind = !!deps['tailwindcss'];
169
+ } catch {}
170
+ }
171
+
172
+ const ngJsonPath = join(root, 'angular.json');
173
+ if (existsSync(ngJsonPath)) {
174
+ try {
175
+ const ngJson = JSON.parse(readFileSync(ngJsonPath, 'utf8'));
176
+ out.ngJsonPath = ngJsonPath;
177
+ out.ngJson = ngJson;
178
+ for (const [name, proj] of Object.entries(ngJson.projects || {})) {
179
+ const build = proj.architect?.build || proj.targets?.build;
180
+ if (build) {
181
+ out.projName = name;
182
+ out.sourceRoot = proj.sourceRoot || 'src';
183
+ const styles = build.options?.styles || [];
184
+ out.stylesFile = styles.find((s) => typeof s === 'string' && /\.(scss|css)$/.test(s)) || `${out.sourceRoot}/styles.scss`;
185
+ break;
186
+ }
187
+ }
188
+ } catch {}
189
+ }
190
+ out.appConfig = join(root, out.sourceRoot, 'app', 'app.config.ts');
191
+ return out;
192
+ }
193
+
194
+ /** Versions CoreUI/CDK alignées sur la version d'Angular du projet. */
195
+ function coreuiVersions(ngMajor) {
196
+ const map = { 19: '^5.4', 20: '^5.5', 21: '^5.6', 22: '^5.6' };
197
+ const cu = map[ngMajor] || '^5.5';
198
+ const ng = ngMajor ? `^${ngMajor}.0.0` : '^20.0.0';
199
+ return {
200
+ list: [`@coreui/angular@${cu}`, '@coreui/coreui@~5.4', `@coreui/icons-angular@${cu}`, `@angular/cdk@${ng}`, `@angular/animations@${ng}`],
201
+ ng,
202
+ };
203
+ }
204
+
205
+ function runNpmInstall(root, pkgs, { dryRun, dev = false } = {}) {
206
+ if (!pkgs.length) return;
207
+ const cmd = `npm install ${dev ? '-D ' : ''}${pkgs.join(' ')} --legacy-peer-deps`;
208
+ console.log(` ${c.cyan('$')} ${cmd}`);
209
+ if (dryRun) return;
210
+ try {
211
+ execSync(cmd, { cwd: root, stdio: 'inherit' });
212
+ } catch {
213
+ console.log(c.yellow(` ⚠ npm install a échoué — lance-le manuellement : ${cmd}`));
214
+ }
215
+ }
216
+
217
+ /** Ajoute une ligne à un fichier si absente (création si besoin). */
218
+ function ensureLine(path, line, { dryRun, prepend = false } = {}) {
219
+ let content = existsSync(path) ? readFileSync(path, 'utf8') : '';
220
+ if (content.includes(line.trim())) return false;
221
+ content = prepend ? `${line}\n${content}` : `${content}${content.endsWith('\n') || content === '' ? '' : '\n'}${line}\n`;
222
+ if (!dryRun) {
223
+ mkdirSync(dirname(path), { recursive: true });
224
+ writeFileSync(path, content);
225
+ }
226
+ return true;
227
+ }
228
+
229
+ /** Crée/complète tailwind.config.js avec le preset partagé. */
230
+ function wireTailwind(root, dryRun) {
231
+ const path = join(root, 'tailwind.config.js');
232
+ if (!existsSync(path)) {
233
+ const content = `/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n presets: [require('./tailwind.preset.cjs')],\n content: ['./src/**/*.{html,ts}'],\n};\n`;
234
+ if (!dryRun) writeFileSync(path, content);
235
+ return 'créé';
236
+ }
237
+ let s = readFileSync(path, 'utf8');
238
+ if (s.includes('tailwind.preset.cjs')) return 'déjà présent';
239
+ const m = s.match(/(module\.exports\s*=\s*\{|export\s+default\s*\{)/);
240
+ if (!m) return 'à compléter à la main';
241
+ const idx = m.index + m[0].length;
242
+ s = `${s.slice(0, idx)}\n presets: [require('./tailwind.preset.cjs')],${s.slice(idx)}`;
243
+ if (!dryRun) writeFileSync(path, s);
244
+ return 'preset ajouté';
245
+ }
246
+
247
+ /** Ajoute le CSS CoreUI au tableau styles d'angular.json. */
248
+ function wireCoreuiCss(proj, dryRun) {
249
+ if (!proj.ngJson || !proj.projName) return 'angular.json introuvable';
250
+ const cssPath = 'node_modules/@coreui/coreui/dist/css/coreui.min.css';
251
+ const p = proj.ngJson.projects[proj.projName];
252
+ const build = p.architect?.build || p.targets?.build;
253
+ build.options = build.options || {};
254
+ build.options.styles = build.options.styles || [];
255
+ if (build.options.styles.includes(cssPath)) return 'déjà présent';
256
+ build.options.styles.unshift(cssPath);
257
+ if (!dryRun) writeFileSync(proj.ngJsonPath, JSON.stringify(proj.ngJson, null, 2) + '\n');
258
+ return 'ajouté';
259
+ }
260
+
261
+ /** Ajoute provideAnimations() dans app.config.ts. */
262
+ function wireProvideAnimations(appConfigPath, dryRun) {
263
+ if (!existsSync(appConfigPath)) return 'app.config.ts introuvable';
264
+ let s = readFileSync(appConfigPath, 'utf8');
265
+ if (s.includes('provideAnimations')) return 'déjà présent';
266
+ if (!s.includes('@angular/platform-browser/animations')) {
267
+ s = `import { provideAnimations } from '@angular/platform-browser/animations';\n${s}`;
268
+ }
269
+ const m = s.match(/providers\s*:\s*\[/);
270
+ if (!m) return 'providers[] introuvable';
271
+ const idx = m.index + m[0].length;
272
+ s = `${s.slice(0, idx)}\n provideAnimations(),${s.slice(idx)}`;
273
+ if (!dryRun) writeFileSync(appConfigPath, s);
274
+ return 'ajouté';
275
+ }
276
+
122
277
  // --- parseur d'arguments ---------------------------------------------------
123
278
  function parseArgs(argv) {
124
279
  const positionals = [];
125
- const flags = { cwd: '.', dir: undefined, overwrite: false, dryRun: false, registry: undefined };
280
+ const flags = {
281
+ cwd: '.', dir: undefined, overwrite: false, dryRun: false,
282
+ registry: undefined, token: undefined, repo: undefined, ref: undefined, install: true,
283
+ };
126
284
  for (let i = 0; i < argv.length; i++) {
127
285
  const a = argv[i];
128
286
  if (a === '--overwrite') flags.overwrite = true;
129
287
  else if (a === '--dry-run') flags.dryRun = true;
288
+ else if (a === '--no-install') flags.install = false;
130
289
  else if (a === '--cwd') flags.cwd = argv[++i];
131
290
  else if (a === '--dir') flags.dir = argv[++i];
132
291
  else if (a === '--registry') flags.registry = argv[++i];
292
+ else if (a === '--token') flags.token = argv[++i];
293
+ else if (a === '--repo') flags.repo = argv[++i];
294
+ else if (a === '--ref') flags.ref = argv[++i];
133
295
  else if (a.startsWith('--')) fail(`Option inconnue: ${a}`);
134
296
  else positionals.push(a);
135
297
  }
@@ -137,7 +299,9 @@ function parseArgs(argv) {
137
299
  }
138
300
 
139
301
  function sourceLabel(source) {
140
- return source.remote ? c.dim(`(registry distant: ${source.base})`) : c.dim('(registry local)');
302
+ if (source.kind === 'local') return c.dim('(registry local)');
303
+ if (source.kind === 'url') return c.dim(`(registry: ${source.base})`);
304
+ return c.dim(`(registry GitHub: ${source.repo}@${source.ref})`);
141
305
  }
142
306
 
143
307
  // --- commandes -------------------------------------------------------------
@@ -222,14 +386,45 @@ async function cmdAdd(names, flags) {
222
386
  }
223
387
 
224
388
  console.log(log.join('\n'));
389
+
390
+ // --- Dépendances npm + câblage CoreUI automatiques ---
391
+ const proj = detectProject(flags.cwd);
392
+ const usesCoreui = npmDeps.has('@coreui/angular');
393
+
394
+ // Liste à installer : deps registry (hors @coreui/angular et @angular/cdk gérés à part) + peers CoreUI alignés
395
+ const installList = [...npmDeps].filter((d) => d !== '@coreui/angular' && d !== '@angular/cdk');
396
+ if (usesCoreui) {
397
+ const { list } = coreuiVersions(proj.ngMajor);
398
+ installList.push(...list);
399
+ console.log(
400
+ c.dim(
401
+ `\n CoreUI détecté → versions alignées sur Angular ${proj.ngMajor ?? '?'} ` +
402
+ `(${coreuiVersions(proj.ngMajor).list.join(' ')})`,
403
+ ),
404
+ );
405
+ } else if (npmDeps.has('@angular/cdk')) {
406
+ installList.push(`@angular/cdk${proj.ngMajor ? '@^' + proj.ngMajor + '.0.0' : ''}`);
407
+ }
408
+
225
409
  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`);
410
+ if (installList.length) {
411
+ if (flags.install) runNpmInstall(proj.root, installList, { dryRun: flags.dryRun });
412
+ else {
413
+ console.log(c.cyan(' Dépendances npm (à installer — sauté: --no-install):'));
414
+ console.log(` ${c.bold('npm install ' + installList.join(' '))}`);
415
+ }
416
+ }
417
+
418
+ // Câblage CoreUI (CSS + animations) si nécessaire
419
+ if (usesCoreui) {
420
+ console.log(c.cyan('\n Câblage CoreUI:'));
421
+ console.log(` ${c.green('✓')} angular.json styles — ${wireCoreuiCss(proj, flags.dryRun)}`);
422
+ console.log(` ${c.green('✓')} app.config.ts provideAnimations() — ${wireProvideAnimations(proj.appConfig, flags.dryRun)}`);
229
423
  }
424
+
230
425
  console.log(
231
426
  c.dim(
232
- ` ${written} fichier(s) ${flags.dryRun ? 'seraient écrits' : 'écrits'}` +
427
+ `\n ${written} fichier(s) ${flags.dryRun ? 'seraient écrits' : 'écrits'}` +
233
428
  (skipped ? `, ${skipped} ignoré(s)` : '') +
234
429
  '.\n',
235
430
  ),
@@ -293,12 +488,30 @@ async function cmdInit(flags) {
293
488
  }
294
489
 
295
490
  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>`)')}`);
491
+
492
+ // --- Câblage automatique du projet cible ---
493
+ const proj = detectProject(flags.cwd);
494
+ console.log(c.cyan('\n Câblage automatique:'));
495
+
496
+ // 1. Fichier de styles global : directives Tailwind (si absentes) + @import tokens
497
+ const stylesFile = proj.stylesFile ? join(proj.root, proj.stylesFile) : join(projectRoot, dir, 'styles.scss');
498
+ if (!existsSync(stylesFile) || !readFileSync(stylesFile, 'utf8').includes('@tailwind base')) {
499
+ ensureLine(stylesFile, '@tailwind base;\n@tailwind components;\n@tailwind utilities;', { dryRun: flags.dryRun, prepend: true });
500
+ }
501
+ const addedImport = ensureLine(stylesFile, `@import './styles/guford-ui-tokens.css';`, { dryRun: flags.dryRun });
502
+ console.log(` ${addedImport ? c.green('✓') : c.dim('•')} tokens importés → ${relative(process.cwd(), stylesFile)}`);
503
+
504
+ // 2. tailwind.config.js (preset partagé)
505
+ console.log(` ${c.green('✓')} tailwind.config.js — ${wireTailwind(proj.root, flags.dryRun)}`);
506
+
507
+ // 3. Peer-deps de base (+ Tailwind si absent)
508
+ if (flags.install) {
509
+ runNpmInstall(proj.root, ['clsx', 'tailwind-merge'], { dryRun: flags.dryRun });
510
+ if (!proj.hasTailwind) runNpmInstall(proj.root, ['tailwindcss@3', 'postcss', 'autoprefixer'], { dryRun: flags.dryRun, dev: true });
511
+ } else {
512
+ console.log(c.dim(' • npm install clsx tailwind-merge' + (proj.hasTailwind ? '' : ' (+ -D tailwindcss@3 postcss autoprefixer)') + ' (sauté: --no-install)'));
513
+ }
514
+
302
515
  console.log(
303
516
  c.dim(
304
517
  `\n ${written} fichier(s) ${flags.dryRun ? 'seraient écrits' : 'écrits'}` +
@@ -321,9 +534,20 @@ function help() {
321
534
  ${c.cyan('Options:')}
322
535
  --cwd <path> Projet cible (défaut: .)
323
536
  --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)
537
+ --registry <url> Base HTTP statique du registry (sinon $GUFORD_UI_REGISTRY)
538
+ --repo <owner/repo> Dépôt GitHub (défaut: ${DEFAULT_REPO})
539
+ --ref <branch|tag> Réf GitHub (défaut: ${DEFAULT_REF}) — pin sur un tag pour reproductibilité
540
+ --token <token> Token GitHub pour dépôt privé (sinon $GUFORD_UI_TOKEN / $GITHUB_TOKEN)
541
+ --no-install Ne lance pas npm install (affiche les commandes)
325
542
  --overwrite Écrase les fichiers existants
326
- --dry-run Simule sans écrire
543
+ --dry-run Simule sans écrire ni installer
544
+
545
+ ${c.cyan('Auto-câblage:')} init/add modifient le projet cible (npm install avec
546
+ CoreUI aligné sur votre Angular, @import des tokens, preset Tailwind,
547
+ CSS CoreUI dans angular.json, provideAnimations dans app.config.ts).
548
+
549
+ ${c.cyan('Dépôt privé:')} crée un PAT GitHub (Contents: read), puis
550
+ ${c.dim('set GUFORD_UI_TOKEN=<token> # Windows (cmd) | $env:GUFORD_UI_TOKEN="<token>" (PowerShell)')}
327
551
  `);
328
552
  }
329
553
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guford-ui",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI pour installer les composants du registry guford-ui (façon shadcn).",
5
5
  "type": "module",
6
6
  "bin": {