grg-kit-cli 0.3.3 → 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/commands/add.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const degit = require('degit');
2
2
  const chalk = require('chalk');
3
3
  const ora = require('ora');
4
- const { RESOURCES, REPO } = require('../config/resources');
4
+ const { fetchCatalog, REPO } = require('../config/catalog-fetcher');
5
5
 
6
6
  /**
7
7
  * Add command - downloads blocks
@@ -12,6 +12,11 @@ const { RESOURCES, REPO } = require('../config/resources');
12
12
  * grg add block --all
13
13
  */
14
14
  async function add(options) {
15
+ // Fetch catalog dynamically (with caching)
16
+ const spinner = ora('Fetching catalog...').start();
17
+ const RESOURCES = await fetchCatalog({ silent: true });
18
+ spinner.stop();
19
+
15
20
  // Determine which blocks to add
16
21
  const blocksToAdd = [];
17
22
 
@@ -44,14 +49,14 @@ async function add(options) {
44
49
 
45
50
  console.log(chalk.bold.cyan(`\nšŸ“¦ Adding ${blocksToAdd.length} block(s)\n`));
46
51
 
47
- const spinner = ora();
52
+ const downloadSpinner = ora();
48
53
 
49
54
  for (const block of blocksToAdd) {
50
55
  const outputPath = options.output
51
56
  ? `${options.output}/${block.name}`
52
57
  : block.defaultOutput;
53
58
 
54
- spinner.start(`Downloading ${block.title}...`);
59
+ downloadSpinner.start(`Downloading ${block.title}...`);
55
60
 
56
61
  try {
57
62
  const emitter = degit(`${REPO}/${block.path}`, {
@@ -62,7 +67,7 @@ async function add(options) {
62
67
 
63
68
  await emitter.clone(outputPath);
64
69
 
65
- spinner.succeed(chalk.green(`āœ“ ${block.title} added`));
70
+ downloadSpinner.succeed(chalk.green(`āœ“ ${block.title} added`));
66
71
  console.log(chalk.gray(` Location: ${outputPath}`));
67
72
 
68
73
  // Show dependencies if any
@@ -72,7 +77,7 @@ async function add(options) {
72
77
  console.log();
73
78
 
74
79
  } catch (error) {
75
- spinner.fail(chalk.red(`Failed to download ${block.title}`));
80
+ downloadSpinner.fail(chalk.red(`Failed to download ${block.title}`));
76
81
  console.error(chalk.red(error.message));
77
82
  }
78
83
  }
package/commands/list.js CHANGED
@@ -1,11 +1,17 @@
1
1
  const chalk = require('chalk');
2
- const { RESOURCES } = require('../config/resources');
2
+ const ora = require('ora');
3
+ const { fetchCatalog } = require('../config/catalog-fetcher');
3
4
 
4
5
  /**
5
6
  * List command - displays available blocks and themes
6
7
  * Usage: grg list [category]
7
8
  */
8
9
  async function list(category) {
10
+ // Fetch catalog dynamically
11
+ const spinner = ora('Fetching catalog...').start();
12
+ const RESOURCES = await fetchCatalog({ silent: true });
13
+ spinner.stop();
14
+
9
15
  if (!category) {
10
16
  // Show overview
11
17
  console.log(chalk.bold.cyan('\nšŸ“¦ GRG Kit Resources\n'));
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Dynamic catalog fetcher with caching
3
+ * Fetches catalog.json from GitHub with fallback to static resources
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const https = require('https');
9
+
10
+ const REPO = 'Genesis-Research/grg-kit';
11
+ const CATALOG_URL = `https://raw.githubusercontent.com/${REPO}/main/templates/catalog.json`;
12
+ const CACHE_FILE = path.join(__dirname, '.catalog-cache.json');
13
+ const CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
14
+
15
+ // In-memory cache for current session
16
+ let memoryCache = null;
17
+ let memoryCacheTime = 0;
18
+
19
+ /**
20
+ * Fetch JSON from URL
21
+ */
22
+ function fetchJson(url) {
23
+ return new Promise((resolve, reject) => {
24
+ https.get(url, (res) => {
25
+ if (res.statusCode !== 200) {
26
+ reject(new Error(`HTTP ${res.statusCode}`));
27
+ return;
28
+ }
29
+
30
+ let data = '';
31
+ res.on('data', chunk => data += chunk);
32
+ res.on('end', () => {
33
+ try {
34
+ resolve(JSON.parse(data));
35
+ } catch (e) {
36
+ reject(new Error('Invalid JSON'));
37
+ }
38
+ });
39
+ }).on('error', reject);
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Read file cache
45
+ */
46
+ function readFileCache() {
47
+ try {
48
+ if (fs.existsSync(CACHE_FILE)) {
49
+ const cached = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
50
+ if (Date.now() - cached.timestamp < CACHE_TTL_MS) {
51
+ return cached.data;
52
+ }
53
+ }
54
+ } catch (e) {
55
+ // Cache read failed, ignore
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Write file cache
62
+ */
63
+ function writeFileCache(data) {
64
+ try {
65
+ fs.writeFileSync(CACHE_FILE, JSON.stringify({
66
+ timestamp: Date.now(),
67
+ data
68
+ }), 'utf-8');
69
+ } catch (e) {
70
+ // Cache write failed, ignore
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get static fallback resources
76
+ */
77
+ function getStaticFallback() {
78
+ try {
79
+ const { RESOURCES } = require('./resources');
80
+ return RESOURCES;
81
+ } catch (e) {
82
+ return { themes: [], components: [], blocks: [] };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Fetch catalog with caching
88
+ * Priority: memory cache -> file cache -> network -> static fallback
89
+ */
90
+ async function fetchCatalog(options = {}) {
91
+ const { forceRefresh = false, silent = false } = options;
92
+
93
+ // Check memory cache first (fastest)
94
+ if (!forceRefresh && memoryCache && (Date.now() - memoryCacheTime < CACHE_TTL_MS)) {
95
+ return memoryCache;
96
+ }
97
+
98
+ // Check file cache
99
+ if (!forceRefresh) {
100
+ const fileCached = readFileCache();
101
+ if (fileCached) {
102
+ memoryCache = fileCached;
103
+ memoryCacheTime = Date.now();
104
+ return fileCached;
105
+ }
106
+ }
107
+
108
+ // Fetch from network
109
+ try {
110
+ const catalog = await fetchJson(CATALOG_URL);
111
+
112
+ // Transform to match expected format
113
+ const resources = {
114
+ themes: catalog.themes.map(t => ({
115
+ ...t,
116
+ path: `templates/ui/themes/${t.file}`,
117
+ defaultOutput: `src/themes/${t.file}`
118
+ })),
119
+ components: catalog.components.map(c => ({
120
+ ...c,
121
+ path: `templates/ui/components/${c.name}`,
122
+ defaultOutput: `src/app/components/${c.name}`
123
+ })),
124
+ blocks: catalog.blocks.map(b => ({
125
+ ...b,
126
+ path: `templates/ui/blocks/${b.name}`,
127
+ defaultOutput: `src/app/blocks/${b.name}`
128
+ }))
129
+ };
130
+
131
+ // Update caches
132
+ memoryCache = resources;
133
+ memoryCacheTime = Date.now();
134
+ writeFileCache(resources);
135
+
136
+ return resources;
137
+ } catch (error) {
138
+ if (!silent) {
139
+ console.warn(`Warning: Could not fetch catalog (${error.message}), using cached/static resources`);
140
+ }
141
+
142
+ // Try file cache even if expired
143
+ try {
144
+ if (fs.existsSync(CACHE_FILE)) {
145
+ const cached = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
146
+ return cached.data;
147
+ }
148
+ } catch (e) {
149
+ // Ignore
150
+ }
151
+
152
+ // Final fallback to static resources
153
+ return getStaticFallback();
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get resources synchronously (uses cache or static fallback)
159
+ */
160
+ function getResourcesSync() {
161
+ // Check memory cache
162
+ if (memoryCache && (Date.now() - memoryCacheTime < CACHE_TTL_MS)) {
163
+ return memoryCache;
164
+ }
165
+
166
+ // Check file cache
167
+ const fileCached = readFileCache();
168
+ if (fileCached) {
169
+ memoryCache = fileCached;
170
+ memoryCacheTime = Date.now();
171
+ return fileCached;
172
+ }
173
+
174
+ // Static fallback
175
+ return getStaticFallback();
176
+ }
177
+
178
+ /**
179
+ * Clear all caches
180
+ */
181
+ function clearCache() {
182
+ memoryCache = null;
183
+ memoryCacheTime = 0;
184
+ try {
185
+ if (fs.existsSync(CACHE_FILE)) {
186
+ fs.unlinkSync(CACHE_FILE);
187
+ }
188
+ } catch (e) {
189
+ // Ignore
190
+ }
191
+ }
192
+
193
+ module.exports = {
194
+ fetchCatalog,
195
+ getResourcesSync,
196
+ clearCache,
197
+ REPO
198
+ };
@@ -133,11 +133,18 @@ const RESOURCES = {
133
133
  {
134
134
  "name": "file-upload",
135
135
  "title": "File Upload Component",
136
- "description": "file-upload component",
136
+ "description": "Drag and drop file upload component",
137
137
  "path": "templates/ui/components/file-upload",
138
138
  "defaultOutput": "src/app/components/file-upload",
139
- "tags": [],
140
- "dependencies": []
139
+ "tags": [
140
+ "file",
141
+ "upload",
142
+ "form",
143
+ "drag-drop"
144
+ ],
145
+ "dependencies": [
146
+ "@spartan-ng/helm/button"
147
+ ]
141
148
  },
142
149
  {
143
150
  "name": "stepper",
@@ -154,23 +161,22 @@ const RESOURCES = {
154
161
  "dependencies": [
155
162
  "@spartan-ng/helm/button",
156
163
  "@spartan-ng/helm/card"
157
- ],
158
- "type": "grg-component",
159
- "prefix": "grg"
164
+ ]
160
165
  }
161
166
  ],
162
167
  "blocks": [
163
168
  {
164
169
  "name": "auth",
165
- "title": "Auth Block",
166
- "description": "Authentication pages layout (login, signup, forgot password)",
170
+ "title": "Auth",
171
+ "description": "Authentication pages (login, signup, forgot password)",
167
172
  "path": "templates/ui/blocks/auth",
168
173
  "defaultOutput": "src/app/blocks/auth",
169
174
  "tags": [
170
175
  "auth",
171
176
  "login",
172
177
  "signup",
173
- "authentication"
178
+ "authentication",
179
+ "form"
174
180
  ],
175
181
  "dependencies": [
176
182
  "@spartan-ng/helm/button",
@@ -180,25 +186,45 @@ const RESOURCES = {
180
186
  },
181
187
  {
182
188
  "name": "settings",
183
- "title": "Settings Block",
184
- "description": "settings block",
189
+ "title": "Settings",
190
+ "description": "Settings pages: profile, notifications, security, danger zone",
185
191
  "path": "templates/ui/blocks/settings",
186
192
  "defaultOutput": "src/app/blocks/settings",
187
193
  "tags": [
188
- "settings"
194
+ "settings",
195
+ "preferences",
196
+ "account",
197
+ "profile",
198
+ "security"
189
199
  ],
190
- "dependencies": []
200
+ "dependencies": [
201
+ "@spartan-ng/helm/button",
202
+ "@spartan-ng/helm/card",
203
+ "@spartan-ng/helm/form-field",
204
+ "@spartan-ng/helm/switch"
205
+ ]
191
206
  },
192
207
  {
193
208
  "name": "shell",
194
- "title": "Shell Block",
195
- "description": "shell block",
209
+ "title": "Shell",
210
+ "description": "Application shell layouts: sidebar, topnav, collapsible - each with optional footer variant",
196
211
  "path": "templates/ui/blocks/shell",
197
212
  "defaultOutput": "src/app/blocks/shell",
198
213
  "tags": [
199
- "shell"
214
+ "shell",
215
+ "layout",
216
+ "sidebar",
217
+ "header",
218
+ "footer",
219
+ "navigation",
220
+ "topnav",
221
+ "collapsible"
200
222
  ],
201
- "dependencies": []
223
+ "dependencies": [
224
+ "@spartan-ng/helm/button",
225
+ "@spartan-ng/helm/icon",
226
+ "@spartan-ng/helm/dropdown-menu"
227
+ ]
202
228
  }
203
229
  ]
204
230
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grg-kit-cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "CLI tool for pulling GRG Kit resources into your Angular project",
5
5
  "main": "index.js",
6
6
  "bin": {
package/scripts/README.md CHANGED
@@ -1,101 +1,233 @@
1
- # CLI Scripts
1
+ # Resource Generation Flow
2
2
 
3
- ## generate-resources.js
3
+ ## Overview
4
4
 
5
- Automatically generates `config/resources.js` by scanning the `templates/` directory.
5
+ GRG Kit uses a **two-stage generation pipeline** to ensure CLI and MCP server always have up-to-date resource definitions.
6
6
 
7
- ### Purpose
7
+ ```
8
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
9
+ │ SOURCE OF TRUTH │
10
+ │ │
11
+ │ app/src/app/blocks/{block}/meta.json ← Block metadata │
12
+ │ app/src/themes/meta.json ← Theme metadata │
13
+ │ app/libs/grg-ui/{component}/meta.json ← Component metadata │
14
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
15
+ │
16
+ ā–¼
17
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
18
+ │ STAGE 1: pnpm generate:sources (in app/) │
19
+ │ │
20
+ │ • Transforms source components → template files │
21
+ │ • Copies meta.json files → templates/ directory │
22
+ │ • Generates generated-sources.ts for showcase app │
23
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
24
+ │
25
+ ā–¼
26
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
27
+ │ TEMPLATES DIRECTORY │
28
+ │ │
29
+ │ templates/ui/blocks/{block}/meta.json ← Copied from app │
30
+ │ templates/ui/blocks/{block}/*.component.ts ← Generated │
31
+ │ templates/ui/themes/meta.json ← Copied from app │
32
+ │ templates/ui/components/{comp}/meta.json ← Copied from app │
33
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
34
+ │
35
+ ā–¼
36
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
37
+ │ STAGE 2: node scripts/generate-resources.js (in cli/) │
38
+ │ │
39
+ │ • Scans templates/ directory │
40
+ │ • Reads meta.json files for metadata │
41
+ │ • Generates cli/config/resources.js (static fallback) │
42
+ │ • Generates templates/catalog.json (dynamic, fetched at runtime) │
43
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
44
+ │
45
+ ā–¼
46
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
47
+ │ RUNTIME (CLI & MCP) │
48
+ │ │
49
+ │ 1. Check memory cache (instant) │
50
+ │ 2. Check file cache (~1ms) │
51
+ │ 3. Fetch catalog.json from GitHub (~100-200ms) │
52
+ │ 4. Fallback to static resources.js │
53
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
54
+ ```
55
+
56
+ ---
8
57
 
9
- Ensures the CLI always has up-to-date resource definitions based on what's actually available in the templates, preventing:
10
- - Outdated resource lists
11
- - Manual maintenance errors
12
- - Mismatched paths
13
- - Missing new resources
58
+ ## Adding New Resources
14
59
 
15
- ### Usage
60
+ ### 1. Add a New Block
16
61
 
17
62
  ```bash
18
- # Run manually
19
- pnpm run generate
63
+ # 1. Create block component in app
64
+ app/src/app/blocks/my-block/my-block.component.ts
20
65
 
21
- # Or directly
22
- node scripts/generate-resources.js
66
+ # 2. Add metadata
67
+ app/src/app/blocks/my-block/meta.json
23
68
  ```
24
69
 
25
- ### Auto-Generation
70
+ ```json
71
+ {
72
+ "description": "Description for AI and CLI",
73
+ "tags": ["keyword1", "keyword2", "searchable"],
74
+ "dependencies": ["@spartan-ng/helm/button", "@spartan-ng/helm/card"]
75
+ }
76
+ ```
26
77
 
27
- The script runs automatically before publishing:
28
78
  ```bash
29
- pnpm publish # Runs prepublishOnly hook → pnpm run generate
79
+ # 3. Update generate-sources.js CONFIG.blocks.sources array
80
+
81
+ # 4. Run generation
82
+ cd app && pnpm generate:sources
83
+ cd ../cli && node scripts/generate-resources.js
84
+
85
+ # 5. Commit and push - CLI/MCP pick up changes automatically
30
86
  ```
31
87
 
32
- ### What It Does
88
+ ### 2. Add a New Theme
33
89
 
34
- 1. **Scans templates directory** for:
35
- - Themes (`templates/ui/themes/*.css`)
36
- - Components (`templates/ui/components/*/`)
37
- - Layouts (`templates/ui/layouts/*/`)
38
- - Examples (`templates/spartan-examples/components/(*)`)
90
+ ```bash
91
+ # 1. Create theme CSS
92
+ app/src/themes/my-theme.css
39
93
 
40
- 2. **Generates metadata** for each resource:
41
- - Name, title, description
42
- - Path and default output location
43
- - Tags for searchability
44
- - Dependencies (if applicable)
94
+ # 2. Add entry to themes meta.json
95
+ app/src/themes/meta.json
96
+ ```
45
97
 
46
- 3. **Writes `config/resources.js`** with structured data
98
+ ```json
99
+ {
100
+ "my-theme.css": {
101
+ "description": "My custom theme description",
102
+ "tags": ["custom", "dark", "modern"]
103
+ }
104
+ }
105
+ ```
47
106
 
48
- ### Output
107
+ ### 3. Add a New GRG Component
108
+
109
+ ```bash
110
+ # 1. Create component in libs/grg-ui
111
+ app/libs/grg-ui/my-component/src/...
49
112
 
113
+ # 2. Add metadata
114
+ app/libs/grg-ui/my-component/meta.json
50
115
  ```
51
- šŸ” Scanning templates directory...
52
- āœ“ Found 6 themes
53
- āœ“ Found 2 components
54
- āœ“ Found 3 layouts
55
- āœ“ Found 56 example components
56
-
57
- āœ… Generated /path/to/config/resources.js
58
-
59
- šŸ“¦ Resource Summary:
60
- Themes: 6
61
- Components: 2
62
- Layouts: 3
63
- Examples: 56
116
+
117
+ ---
118
+
119
+ ## Scripts
120
+
121
+ ### `app/scripts/generate-sources.js`
122
+
123
+ Generates template files and copies metadata.
124
+
125
+ ```bash
126
+ cd app
127
+ pnpm generate:sources
64
128
  ```
65
129
 
66
- ### Customizing Metadata
130
+ **What it does:**
131
+ - Transforms block components → standalone template files
132
+ - Copies `meta.json` files to `templates/` directory
133
+ - Generates `generated-sources.ts` for showcase app
67
134
 
68
- Edit the metadata constants in `generate-resources.js`:
135
+ ### `cli/scripts/generate-resources.js`
69
136
 
70
- ```javascript
71
- const THEME_METADATA = {
72
- 'grg-theme.css': {
73
- description: 'Default theme with purple/orange accents',
74
- tags: ['default', 'purple', 'orange', 'colorful']
75
- }
76
- };
137
+ Generates CLI resources and dynamic catalog.
77
138
 
78
- const COMPONENT_METADATA = {
79
- 'stepper': {
80
- description: 'Multi-step form component',
81
- tags: ['form', 'wizard'],
82
- dependencies: ['@spartan-ng/helm/button']
83
- }
84
- };
139
+ ```bash
140
+ cd cli
141
+ node scripts/generate-resources.js
85
142
  ```
86
143
 
87
- ### Benefits
144
+ **What it does:**
145
+ - Scans `templates/` directory
146
+ - Reads `meta.json` files for metadata
147
+ - Generates `cli/config/resources.js` (static fallback)
148
+ - Generates `templates/catalog.json` (dynamic catalog)
149
+
150
+ ---
151
+
152
+ ## Dynamic Catalog Fetching
153
+
154
+ CLI and MCP server fetch `catalog.json` from GitHub at runtime with caching:
155
+
156
+ | Cache Level | TTL | Speed |
157
+ |-------------|-----|-------|
158
+ | Memory cache | 15 min | <1ms |
159
+ | File cache | 15 min | ~1ms |
160
+ | GitHub fetch | - | ~100-200ms |
161
+ | Static fallback | - | <1ms |
162
+
163
+ **Benefits:**
164
+ - No CLI/MCP redeploy needed for new resources
165
+ - Changes propagate within 15 minutes
166
+ - Graceful fallback if network fails
167
+
168
+ ---
169
+
170
+ ## File Structure
171
+
172
+ ```
173
+ grg-kit/
174
+ ā”œā”€ā”€ app/
175
+ │ ā”œā”€ā”€ src/
176
+ │ │ ā”œā”€ā”€ app/blocks/
177
+ │ │ │ ā”œā”€ā”€ auth/
178
+ │ │ │ │ ā”œā”€ā”€ meta.json ← Source metadata
179
+ │ │ │ │ └── *.component.ts
180
+ │ │ │ ā”œā”€ā”€ shell/
181
+ │ │ │ │ ā”œā”€ā”€ meta.json
182
+ │ │ │ │ └── *.component.ts
183
+ │ │ │ └── settings/
184
+ │ │ │ ā”œā”€ā”€ meta.json
185
+ │ │ │ └── *.component.ts
186
+ │ │ └── themes/
187
+ │ │ ā”œā”€ā”€ meta.json ← All themes metadata
188
+ │ │ └── *.css
189
+ │ ā”œā”€ā”€ libs/grg-ui/
190
+ │ │ ā”œā”€ā”€ stepper/meta.json
191
+ │ │ └── file-upload/meta.json
192
+ │ └── scripts/
193
+ │ └── generate-sources.js ← Stage 1
194
+ │
195
+ ā”œā”€ā”€ cli/
196
+ │ ā”œā”€ā”€ config/
197
+ │ │ ā”œā”€ā”€ resources.js ← Generated (static fallback)
198
+ │ │ └── catalog-fetcher.js ← Dynamic fetcher
199
+ │ └── scripts/
200
+ │ └── generate-resources.js ← Stage 2
201
+ │
202
+ ā”œā”€ā”€ mcp-server/
203
+ │ └── src/
204
+ │ ā”œā”€ā”€ index.ts
205
+ │ └── catalog-fetcher.ts ← Dynamic fetcher
206
+ │
207
+ └── templates/
208
+ ā”œā”€ā”€ catalog.json ← Generated (dynamic)
209
+ └── ui/
210
+ ā”œā”€ā”€ blocks/
211
+ │ ā”œā”€ā”€ auth/
212
+ │ │ ā”œā”€ā”€ meta.json ← Copied from app
213
+ │ │ └── *.component.ts ← Generated
214
+ │ └── ...
215
+ ā”œā”€ā”€ themes/
216
+ │ ā”œā”€ā”€ meta.json ← Copied from app
217
+ │ └── *.css
218
+ └── components/
219
+ ā”œā”€ā”€ stepper/meta.json ← Copied from app
220
+ └── file-upload/meta.json
221
+ ```
88
222
 
89
- āœ… **Always accurate** - Resources match what's in templates
90
- āœ… **No manual updates** - Add files to templates, run script
91
- āœ… **Type-safe** - Consistent structure for all resources
92
- āœ… **MCP-ready** - Structured metadata for LLM consumption
93
- āœ… **Auto-discovery** - New resources automatically included
223
+ ---
94
224
 
95
- ### Development Workflow
225
+ ## Quick Reference
96
226
 
97
- 1. Add new resource to `templates/` directory
98
- 2. (Optional) Add metadata to script constants
99
- 3. Run `pnpm run generate`
100
- 4. Test with `grg list` or `grg add`
101
- 5. Publish with `pnpm publish` (auto-generates)
227
+ | Task | Command |
228
+ |------|---------|
229
+ | Generate templates + copy meta | `cd app && pnpm generate:sources` |
230
+ | Generate resources + catalog | `cd cli && node scripts/generate-resources.js` |
231
+ | Full regeneration | Run both above |
232
+ | Test CLI | `cd cli && node bin/grg.js list` |
233
+ | Build MCP | `cd mcp-server && pnpm build` |
@@ -10,59 +10,48 @@ const path = require('path');
10
10
 
11
11
  const TEMPLATES_DIR = path.join(__dirname, '../../templates');
12
12
  const OUTPUT_FILE = path.join(__dirname, '../config/resources.js');
13
+ const CATALOG_FILE = path.join(TEMPLATES_DIR, 'catalog.json');
13
14
 
14
- // Theme metadata (can be enhanced by reading CSS files)
15
- const THEME_METADATA = {
16
- 'grg-theme.css': {
17
- description: 'Default theme with purple/orange accents',
18
- tags: ['default', 'purple', 'orange', 'colorful']
19
- },
20
- 'claude.css': {
21
- description: 'Claude-inspired warm tones',
22
- tags: ['warm', 'orange', 'brown', 'claude']
23
- },
24
- 'clean-slate.css': {
25
- description: 'Minimal grayscale palette',
26
- tags: ['minimal', 'grayscale', 'neutral', 'clean']
27
- },
28
- 'modern-minimal.css': {
29
- description: 'Contemporary minimal design',
30
- tags: ['minimal', 'modern', 'contemporary', 'clean']
31
- },
32
- 'amber-minimal.css': {
33
- description: 'Warm amber accents',
34
- tags: ['minimal', 'warm', 'amber', 'orange']
35
- },
36
- 'mocks.css': {
37
- description: 'Theme for mockups and prototypes',
38
- tags: ['mockup', 'prototype', 'design']
39
- }
40
- };
41
-
42
- // Component metadata
43
- const COMPONENT_METADATA = {
44
- 'stepper': {
45
- description: 'Multi-step form component with progress indicator',
46
- tags: ['form', 'wizard', 'multi-step', 'progress'],
47
- dependencies: ['@spartan-ng/helm/button', '@spartan-ng/helm/card'],
48
- type: 'grg-component',
49
- prefix: 'grg'
50
- }
51
- };
52
-
53
- // Block metadata (formerly layouts)
54
- const BLOCK_METADATA = {
55
- 'dashboard': {
56
- description: 'Full dashboard layout with sidebar and header',
57
- tags: ['dashboard', 'admin', 'sidebar', 'navigation'],
58
- dependencies: ['@spartan-ng/helm/button', '@spartan-ng/helm/card', '@spartan-ng/helm/navigation-menu']
59
- },
60
- 'auth': {
61
- description: 'Authentication pages layout (login, signup, forgot password)',
62
- tags: ['auth', 'login', 'signup', 'authentication'],
63
- dependencies: ['@spartan-ng/helm/button', '@spartan-ng/helm/card', '@spartan-ng/helm/form-field']
15
+ /**
16
+ * Read meta.json from a directory if it exists
17
+ */
18
+ function readMeta(dir) {
19
+ const metaPath = path.join(dir, 'meta.json');
20
+ try {
21
+ if (fs.existsSync(metaPath)) {
22
+ return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
23
+ }
24
+ } catch (error) {
25
+ console.warn(`Warning: Could not read ${metaPath}`);
64
26
  }
65
- };
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Default metadata generators
32
+ */
33
+ function defaultBlockMeta(name) {
34
+ return {
35
+ description: `${name} block`,
36
+ tags: [name],
37
+ dependencies: []
38
+ };
39
+ }
40
+
41
+ function defaultComponentMeta(name) {
42
+ return {
43
+ description: `${name} component`,
44
+ tags: [],
45
+ dependencies: []
46
+ };
47
+ }
48
+
49
+ function defaultThemeMeta(name) {
50
+ return {
51
+ description: `${name} theme`,
52
+ tags: []
53
+ };
54
+ }
66
55
 
67
56
  function scanDirectory(dir) {
68
57
  try {
@@ -77,14 +66,14 @@ function generateThemes() {
77
66
  const themesDir = path.join(TEMPLATES_DIR, 'ui/themes');
78
67
  const files = scanDirectory(themesDir);
79
68
 
69
+ // Read themes meta.json (contains all theme metadata keyed by filename)
70
+ const themesMeta = readMeta(themesDir) || {};
71
+
80
72
  return files
81
73
  .filter(file => file.isFile() && file.name.endsWith('.css'))
82
74
  .map(file => {
83
75
  const name = file.name.replace('.css', '');
84
- const metadata = THEME_METADATA[file.name] || {
85
- description: `${name} theme`,
86
- tags: []
87
- };
76
+ const metadata = themesMeta[file.name] || defaultThemeMeta(name);
88
77
 
89
78
  return {
90
79
  name,
@@ -93,7 +82,7 @@ function generateThemes() {
93
82
  file: file.name,
94
83
  path: `templates/ui/themes/${file.name}`,
95
84
  defaultOutput: `src/themes/${file.name}`,
96
- tags: metadata.tags,
85
+ tags: metadata.tags || [],
97
86
  features: ['dark-mode', 'tailwind-v4', 'spartan-ng', 'oklch']
98
87
  };
99
88
  });
@@ -106,11 +95,8 @@ function generateComponents() {
106
95
  return dirs
107
96
  .filter(dir => dir.isDirectory())
108
97
  .map(dir => {
109
- const metadata = COMPONENT_METADATA[dir.name] || {
110
- description: `${dir.name} component`,
111
- tags: [],
112
- dependencies: []
113
- };
98
+ const componentDir = path.join(componentsDir, dir.name);
99
+ const metadata = readMeta(componentDir) || defaultComponentMeta(dir.name);
114
100
 
115
101
  return {
116
102
  name: dir.name,
@@ -126,27 +112,150 @@ function generateComponents() {
126
112
  });
127
113
  }
128
114
 
129
- function generateBlocks() {
115
+ /**
116
+ * Convert filename to readable title
117
+ * e.g., 'sidebar-shell-footer.component.ts' -> 'Sidebar Shell Footer'
118
+ */
119
+ function fileToTitle(filename) {
120
+ return filename
121
+ .replace('.component.ts', '')
122
+ .split('-')
123
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
124
+ .join(' ');
125
+ }
126
+
127
+ /**
128
+ * Convert filename to id
129
+ * e.g., 'sidebar-shell-footer.component.ts' -> 'sidebar-footer'
130
+ */
131
+ function fileToId(filename, blockName) {
132
+ const base = filename.replace('.component.ts', '');
133
+ // Remove block name prefix if present (e.g., 'sidebar-shell' -> 'sidebar')
134
+ return base.replace(`-${blockName}`, '').replace(`${blockName}-`, '');
135
+ }
136
+
137
+ /**
138
+ * Scan block directory for individual component files
139
+ */
140
+ function scanBlockFiles(blockDir, blockName) {
141
+ const files = scanDirectory(blockDir);
142
+
143
+ return files
144
+ .filter(file => file.isFile() && file.name.endsWith('.component.ts'))
145
+ .map(file => {
146
+ const id = fileToId(file.name, blockName);
147
+ const title = fileToTitle(file.name);
148
+
149
+ return {
150
+ id,
151
+ file: file.name,
152
+ title,
153
+ description: title
154
+ };
155
+ })
156
+ .sort((a, b) => a.file.localeCompare(b.file));
157
+ }
158
+
159
+ function generateBlocks(includeFiles = false) {
130
160
  const blocksDir = path.join(TEMPLATES_DIR, 'ui/blocks');
131
161
  const dirs = scanDirectory(blocksDir);
132
162
 
133
163
  return dirs
134
164
  .filter(dir => dir.isDirectory())
135
165
  .map(dir => {
136
- const metadata = BLOCK_METADATA[dir.name] || {
137
- description: `${dir.name} block`,
138
- tags: [dir.name],
139
- dependencies: []
140
- };
166
+ const blockDir = path.join(blocksDir, dir.name);
167
+ const metadata = readMeta(blockDir) || defaultBlockMeta(dir.name);
141
168
 
142
- return {
169
+ const block = {
143
170
  name: dir.name,
144
- title: dir.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') + ' Block',
171
+ title: dir.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
145
172
  description: metadata.description,
146
173
  path: `templates/ui/blocks/${dir.name}`,
147
174
  defaultOutput: `src/app/blocks/${dir.name}`,
148
- tags: metadata.tags,
149
- dependencies: metadata.dependencies
175
+ tags: metadata.tags || [],
176
+ dependencies: metadata.dependencies || []
177
+ };
178
+
179
+ // Include file-level details for catalog
180
+ if (includeFiles) {
181
+ block.files = scanBlockFiles(blockDir, dir.name);
182
+ }
183
+
184
+ return block;
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Generate catalog.json for themes (with features)
190
+ */
191
+ function generateThemesForCatalog() {
192
+ const themesDir = path.join(TEMPLATES_DIR, 'ui/themes');
193
+ const files = scanDirectory(themesDir);
194
+
195
+ // Read themes meta.json
196
+ const themesMeta = readMeta(themesDir) || {};
197
+
198
+ return files
199
+ .filter(file => file.isFile() && file.name.endsWith('.css'))
200
+ .map(file => {
201
+ const name = file.name.replace('.css', '');
202
+ const metadata = themesMeta[file.name] || defaultThemeMeta(name);
203
+
204
+ return {
205
+ name,
206
+ title: name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
207
+ description: metadata.description,
208
+ file: file.name,
209
+ tags: metadata.tags || [],
210
+ features: ['dark-mode', 'tailwind-v4', 'spartan-ng', 'oklch']
211
+ };
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Generate catalog.json for components
217
+ */
218
+ function generateComponentsForCatalog() {
219
+ const componentsDir = path.join(TEMPLATES_DIR, 'ui/components');
220
+ const dirs = scanDirectory(componentsDir);
221
+
222
+ return dirs
223
+ .filter(dir => dir.isDirectory())
224
+ .map(dir => {
225
+ const componentDir = path.join(componentsDir, dir.name);
226
+ const metadata = readMeta(componentDir) || defaultComponentMeta(dir.name);
227
+
228
+ return {
229
+ name: dir.name,
230
+ title: dir.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
231
+ description: metadata.description,
232
+ tags: metadata.tags || [],
233
+ dependencies: metadata.dependencies || []
234
+ };
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Generate catalog.json for blocks (with files)
240
+ */
241
+ function generateBlocksForCatalog() {
242
+ const blocksDir = path.join(TEMPLATES_DIR, 'ui/blocks');
243
+ const dirs = scanDirectory(blocksDir);
244
+
245
+ return dirs
246
+ .filter(dir => dir.isDirectory())
247
+ .map(dir => {
248
+ const blockDir = path.join(blocksDir, dir.name);
249
+ const metadata = readMeta(blockDir) || defaultBlockMeta(dir.name);
250
+ const files = scanBlockFiles(blockDir, dir.name);
251
+
252
+ return {
253
+ name: dir.name,
254
+ title: dir.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
255
+ description: metadata.description,
256
+ tags: metadata.tags || [],
257
+ dependencies: metadata.dependencies,
258
+ files
150
259
  };
151
260
  });
152
261
  }
@@ -162,6 +271,7 @@ function generateResourcesFile() {
162
271
  console.log(`āœ“ Found ${components.length} components`);
163
272
  console.log(`āœ“ Found ${blocks.length} blocks`);
164
273
 
274
+ // Generate resources.js (static fallback for CLI)
165
275
  const output = `/**
166
276
  * Resource definitions for GRG Kit
167
277
  * This file is auto-generated from templates directory
@@ -180,10 +290,29 @@ module.exports = { RESOURCES, REPO };
180
290
 
181
291
  fs.writeFileSync(OUTPUT_FILE, output);
182
292
  console.log(`\nāœ… Generated ${OUTPUT_FILE}`);
293
+
294
+ // Generate catalog.json (dynamic, fetched at runtime)
295
+ const catalogThemes = generateThemesForCatalog();
296
+ const catalogComponents = generateComponentsForCatalog();
297
+ const catalogBlocks = generateBlocksForCatalog();
298
+
299
+ const totalFiles = catalogBlocks.reduce((sum, b) => sum + (b.files?.length || 0), 0);
300
+
301
+ const catalog = {
302
+ version: '1.0.0',
303
+ lastUpdated: new Date().toISOString().split('T')[0],
304
+ themes: catalogThemes,
305
+ components: catalogComponents,
306
+ blocks: catalogBlocks
307
+ };
308
+
309
+ fs.writeFileSync(CATALOG_FILE, JSON.stringify(catalog, null, 2));
310
+ console.log(`āœ… Generated ${CATALOG_FILE}`);
311
+
183
312
  console.log('\nšŸ“¦ Resource Summary:');
184
313
  console.log(` Themes: ${themes.length}`);
185
314
  console.log(` Components: ${components.length}`);
186
- console.log(` Blocks: ${blocks.length}`);
315
+ console.log(` Blocks: ${blocks.length} (${totalFiles} files)`);
187
316
  }
188
317
 
189
318
  // Run the generator