wad-lab 1.2.0 → 1.2.1

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/index.js +734 -126
  2. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -41,114 +41,556 @@ const frontendOptions = [
41
41
  { name: 'vue-axios-interceptor (Vue 3 + Axios Interceptors + Toast Alerts)', value: 'vue-axios-interceptor' }
42
42
  ];
43
43
 
44
+ const categories = [
45
+ {
46
+ id: 'fullstack',
47
+ title: 'Fullstack Web Applications',
48
+ icon: '🌐',
49
+ desc: 'Integrated Spring Boot + MongoDB + Vue 3 apps'
50
+ },
51
+ {
52
+ id: 'springboot',
53
+ title: 'Spring Boot Backend APIs',
54
+ icon: '🍃',
55
+ desc: 'REST APIs, MongoDB, Aggregations, Join & MockMvc'
56
+ },
57
+ {
58
+ id: 'servlet',
59
+ title: 'Java Servlets & Tomcat 9',
60
+ icon: '☕',
61
+ desc: 'Classic Tomcat Servlets, JDBC, Sessions & Web.xml'
62
+ },
63
+ {
64
+ id: 'vue-axios',
65
+ title: 'Vue 3 + Axios HTTP Variants',
66
+ icon: '⚡',
67
+ desc: 'All 10+ architectural combinations of Vue 3 with Axios'
68
+ },
69
+ {
70
+ id: 'vue-core',
71
+ title: 'Vue 3 Core Portals & State',
72
+ icon: '🎨',
73
+ desc: 'Router, Pinia, Native Fetch & Complete UI Portals'
74
+ },
75
+ {
76
+ id: 'games',
77
+ title: 'Interactive Games (JS & Vue)',
78
+ icon: '🎮',
79
+ desc: 'Logic, DOM & State Management Games'
80
+ },
81
+ {
82
+ id: 'html-css',
83
+ title: 'HTML5 & CSS3 Foundations',
84
+ icon: '📄',
85
+ desc: 'Form Validation, DOM Manipulation & 50 CSS Properties'
86
+ }
87
+ ];
88
+
44
89
  const templates = [
45
- { name: 'all (Unpack All 56 Templates)', value: 'all' },
46
- { name: 'servlet-voter-eligibility (Name & Age Voting Eligibility Check)', value: 'servlet-voter-eligibility' },
47
- { name: 'fullstack-springboot-vue (Spring Boot: 2 Models, 2 Repos, 1 Service, 1 Controller + Selectable Vue 3 Axios Frontend)', value: 'fullstack-springboot-vue' },
48
- { name: 'springboot-2model-1controller-api (Spring Boot 1 Service, 1 Controller, 2 Models, 2 Repos)', value: 'springboot-2model-1controller-api' },
49
- { name: 'vue-axios-basic (Vue 3 + Axios Direct CRUD)', value: 'vue-axios-basic' },
50
- { name: 'vue-axios-props-emits (Vue 3 + Axios + Props & Emits)', value: 'vue-axios-props-emits' },
51
- { name: 'vue-axios-router (Vue 3 + Axios + Router Views)', value: 'vue-axios-router' },
52
- { name: 'vue-axios-pinia (Vue 3 + Axios + Pinia Centralized Store)', value: 'vue-axios-pinia' },
53
- { name: 'vue-axios-router-props (Vue 3 + Axios + Router + Props/Emits)', value: 'vue-axios-router-props' },
54
- { name: 'vue-axios-pinia-props (Vue 3 + Axios + Pinia + Props/Emits)', value: 'vue-axios-pinia-props' },
55
- { name: 'vue-axios-router-pinia (Vue 3 + Axios + Router + Pinia)', value: 'vue-axios-router-pinia' },
56
- { name: 'vue-axios-router-pinia-props (The Kitchen Sink — Router + Pinia + Props + Emits)', value: 'vue-axios-router-pinia-props' },
57
- { name: 'vue-axios-router-children (Vue 3 + Axios + Router Nested Children)', value: 'vue-axios-router-children' },
58
- { name: 'vue-axios-interceptor (Vue 3 + Axios Interceptors + Toast Alerts)', value: 'vue-axios-interceptor' },
59
- { name: 'vue-basics (ref, reactive, computed, v-model)', value: 'vue-basics' },
60
- { name: 'vue-props-emits (Parent/Child Component Communication)', value: 'vue-props-emits' },
61
- { name: 'vue-router (Parent Layout & Nested Children)', value: 'vue-router' },
62
- { name: 'vue-pinia (Centralized State Store)', value: 'vue-pinia' },
63
- { name: 'vue-router-pinia (Router with Children + Pinia)', value: 'vue-router-pinia' },
64
- { name: 'vue-router-pinia-props (Router + Pinia + Props + Emits)', value: 'vue-router-pinia-props' },
65
- { name: 'vue-fetch (Native fetch CRUD)', value: 'vue-fetch' },
66
- { name: 'vue-axios (Axios CRUD Service)', value: 'vue-axios' },
67
- { name: 'vue-todo-pinia (Pinia Task Manager)', value: 'vue-todo-pinia' },
68
- { name: 'vue-todo-router (Router Multi-View Tasks)', value: 'vue-todo-router' },
69
- { name: 'vue-cart-router-pinia (Router + Pinia Shopping Cart)', value: 'vue-cart-router-pinia' },
70
- { name: 'html-css-js (Form Validation & DOM)', value: 'html-css-js' },
71
- { name: 'html-css-styles (50 CSS properties showcase)', value: 'html-css-styles' },
72
- { name: 'servlet-basic (Tomcat GET + POST)', value: 'servlet-basic' },
73
- { name: 'servlet-jdbc (Tomcat + MySQL JDBC)', value: 'servlet-jdbc' },
74
- { name: 'servlet-flow (HelloServlet with GET/POST)', value: 'servlet-flow' },
75
- { name: 'servlet-session-tracking (HttpSession, Cookies, Redirect, URL Rewriting)', value: 'servlet-session-tracking' },
76
- { name: 'springboot-mongo-crud (4-Layer REST API)', value: 'springboot-mongo-crud' },
77
- { name: 'springboot-mongo-aggregation ($group, $sum, $avg, $sort)', value: 'springboot-mongo-aggregation' },
78
- { name: 'springboot-mongo-aggregation-join ($lookup JOIN, @Query, @Aggregation)', value: 'springboot-mongo-aggregation-join' },
79
- { name: 'springboot-mockmvc (Controller Tests)', value: 'springboot-mockmvc' },
80
- { name: 'springboot-mongo-aggregation-mockmvc (Full Tests)', value: 'springboot-mongo-aggregation-mockmvc' },
81
- { name: 'game-rock-paper-scissors (Player vs Computer RPS)', value: 'game-rock-paper-scissors' },
82
- { name: 'game-tic-tac-toe (3x3 Turn Grid with Win Check)', value: 'game-tic-tac-toe' },
83
- { name: 'game-whack-a-mole (3x3 Mole Whack with Timer)', value: 'game-whack-a-mole' },
84
- { name: 'game-memory-cards (3x4 Flashcard Matching Game)', value: 'game-memory-cards' },
85
- { name: 'game-bingo (Exercise 3 — JavaScript Bingo Game)', value: 'game-bingo' },
86
- { name: 'vue-game-rps (Vue 3 + Pinia Rock Paper Scissors)', value: 'vue-game-rps' },
87
- { name: 'vue-game-tictactoe (Vue 3 + Pinia Tic Tac Toe)', value: 'vue-game-tictactoe' },
88
- { name: 'vue-game-whackamole (Vue 3 + Pinia Whack A Mole)', value: 'vue-game-whackamole' },
89
- { name: 'vue-memory-game (Vue 3 + Pinia Flashcard Matching)', value: 'vue-memory-game' },
90
- { name: 'vue-springboot-mongo-crud (Fullstack CRUD)', value: 'vue-springboot-mongo-crud' },
91
- { name: 'vue-springboot-mongo-aggregation (Fullstack Dashboard)', value: 'vue-springboot-mongo-aggregation' },
92
- { name: 'vue-router-pinia-springboot (Fullstack Enterprise)', value: 'vue-router-pinia-springboot' },
93
- { name: 'vue-food-ordering (Exercise 7 — Online Food Ordering App)', value: 'vue-food-ordering' },
94
- { name: 'vue-symposium-system (Exercise 6 — College Symposium Management)', value: 'vue-symposium-system' },
95
- { name: 'vue-banking-app (Variant 2 — Banking Portal & Ledger)', value: 'vue-banking-app' },
96
- { name: 'vue-employee-system (Variant 1 — Employee HR & Payroll)', value: 'vue-employee-system' },
97
- { name: 'vue-hospital-portal (Variant 3 — Hospital OPD & Vitals Portal)', value: 'vue-hospital-portal' },
98
- { name: 'vue-library-system (Variant 4 — Library Book Loans & Members)', value: 'vue-library-system' },
99
- { name: 'springboot-course-registration (Exercise 5 — Course Registration REST API)', value: 'springboot-course-registration' },
100
- { name: 'springboot-hospital-api (Variant 3 — Hospital Appointments API)', value: 'springboot-hospital-api' },
101
- { name: 'springboot-library-api (Variant 4 — Library Catalog & Analytics API)', value: 'springboot-library-api' }
90
+ // Fullstack (4)
91
+ {
92
+ category: 'fullstack',
93
+ value: 'fullstack-springboot-vue',
94
+ name: 'fullstack-springboot-vue (Spring Boot 2-Model API + Selectable Vue 3 Frontend)'
95
+ },
96
+ {
97
+ category: 'fullstack',
98
+ value: 'vue-springboot-mongo-crud',
99
+ name: 'vue-springboot-mongo-crud (Spring Boot + MongoDB + Vue 3 Direct CRUD)'
100
+ },
101
+ {
102
+ category: 'fullstack',
103
+ value: 'vue-springboot-mongo-aggregation',
104
+ name: 'vue-springboot-mongo-aggregation (Spring Boot Aggregation + Vue 3 Dashboard)'
105
+ },
106
+ {
107
+ category: 'fullstack',
108
+ value: 'vue-router-pinia-springboot',
109
+ name: 'vue-router-pinia-springboot (Fullstack Enterprise: Router + Pinia + Spring Boot)'
110
+ },
111
+
112
+ // Spring Boot (9)
113
+ {
114
+ category: 'springboot',
115
+ value: 'springboot-mongo-crud',
116
+ name: 'springboot-mongo-crud (4-Layer REST API: Controller -> Service -> Repo -> Mongo)'
117
+ },
118
+ {
119
+ category: 'springboot',
120
+ value: 'springboot-mongo-aggregation',
121
+ name: 'springboot-mongo-aggregation (Aggregation Pipeline: $group, $sum, $avg, $sort)'
122
+ },
123
+ {
124
+ category: 'springboot',
125
+ value: 'springboot-mongo-aggregation-join',
126
+ name: 'springboot-mongo-aggregation-join (MongoDB $lookup JOIN, @Query, @Aggregation)'
127
+ },
128
+ {
129
+ category: 'springboot',
130
+ value: 'springboot-mockmvc',
131
+ name: 'springboot-mockmvc (Controller Unit Testing with MockMvc)'
132
+ },
133
+ {
134
+ category: 'springboot',
135
+ value: 'springboot-mongo-aggregation-mockmvc',
136
+ name: 'springboot-mongo-aggregation-mockmvc (Full Testing Lab: MockMvc + Aggregation)'
137
+ },
138
+ {
139
+ category: 'springboot',
140
+ value: 'springboot-2model-1controller-api',
141
+ name: 'springboot-2model-1controller-api (2 Models Dept & Emp, 2 Repos, 1 Service, 1 Controller)'
142
+ },
143
+ {
144
+ category: 'springboot',
145
+ value: 'springboot-course-registration',
146
+ name: 'springboot-course-registration (Exercise 5 — Course Registration REST API)'
147
+ },
148
+ {
149
+ category: 'springboot',
150
+ value: 'springboot-hospital-api',
151
+ name: 'springboot-hospital-api (Hospital OPD Appointments & Doctors API)'
152
+ },
153
+ {
154
+ category: 'springboot',
155
+ value: 'springboot-library-api',
156
+ name: 'springboot-library-api (Library Catalog, Loans & Analytics API)'
157
+ },
158
+
159
+ // Servlets (5)
160
+ {
161
+ category: 'servlet',
162
+ value: 'servlet-basic',
163
+ name: 'servlet-basic (Tomcat GET + POST Basic Servlet)'
164
+ },
165
+ {
166
+ category: 'servlet',
167
+ value: 'servlet-flow',
168
+ name: 'servlet-flow (HelloServlet Execution Flow with GET/POST Form)'
169
+ },
170
+ {
171
+ category: 'servlet',
172
+ value: 'servlet-jdbc',
173
+ name: 'servlet-jdbc (Tomcat + MySQL JDBC Database Connection)'
174
+ },
175
+ {
176
+ category: 'servlet',
177
+ value: 'servlet-session-tracking',
178
+ name: 'servlet-session-tracking (HttpSession, Cookies, Redirect & URL Rewriting)'
179
+ },
180
+ {
181
+ category: 'servlet',
182
+ value: 'servlet-voter-eligibility',
183
+ name: 'servlet-voter-eligibility (Voting Eligibility Check: Name & Age Validation)'
184
+ },
185
+
186
+ // Vue + Axios (11)
187
+ {
188
+ category: 'vue-axios',
189
+ value: 'vue-axios-basic',
190
+ name: 'vue-axios-basic (Vue 3 + Axios Direct CRUD in Single View)'
191
+ },
192
+ {
193
+ category: 'vue-axios-props-emits',
194
+ value: 'vue-axios-props-emits',
195
+ name: 'vue-axios-props-emits (Vue 3 + Axios + Parent/Child Props & Emits)'
196
+ },
197
+ {
198
+ category: 'vue-axios-router',
199
+ value: 'vue-axios-router',
200
+ name: 'vue-axios-router (Vue 3 + Axios + Router Views Dashboard & Details)'
201
+ },
202
+ {
203
+ category: 'vue-axios-pinia',
204
+ value: 'vue-axios-pinia',
205
+ name: 'vue-axios-pinia (Vue 3 + Axios + Pinia Centralized State Store)'
206
+ },
207
+ {
208
+ category: 'vue-axios-router-props',
209
+ value: 'vue-axios-router-props',
210
+ name: 'vue-axios-router-props (Vue 3 + Axios + Router + Component Props/Emits)'
211
+ },
212
+ {
213
+ category: 'vue-axios-pinia-props',
214
+ value: 'vue-axios-pinia-props',
215
+ name: 'vue-axios-pinia-props (Vue 3 + Axios + Pinia Store + Props/Emits)'
216
+ },
217
+ {
218
+ category: 'vue-axios-router-pinia',
219
+ value: 'vue-axios-router-pinia',
220
+ name: 'vue-axios-router-pinia (Vue 3 + Axios + Router Views + Pinia Store)'
221
+ },
222
+ {
223
+ category: 'vue-axios-router-pinia-props',
224
+ value: 'vue-axios-router-pinia-props',
225
+ name: 'vue-axios-router-pinia-props (The Complete Stack: Router + Pinia + Props + Emits)'
226
+ },
227
+ {
228
+ category: 'vue-axios-router-children',
229
+ value: 'vue-axios-router-children',
230
+ name: 'vue-axios-router-children (Vue 3 + Axios + Router Nested Children Layouts)'
231
+ },
232
+ {
233
+ category: 'vue-axios-interceptor',
234
+ value: 'vue-axios-interceptor',
235
+ name: 'vue-axios-interceptor (Vue 3 + Axios Interceptors + Toast Notifications)'
236
+ },
237
+ {
238
+ category: 'vue-axios',
239
+ value: 'vue-axios',
240
+ name: 'vue-axios (Classic Vue 3 Axios Service Layer CRUD)'
241
+ },
242
+
243
+ // Vue Core & Portals (16)
244
+ {
245
+ category: 'vue-core',
246
+ value: 'vue-basics',
247
+ name: 'vue-basics (Core Reactivity: ref, reactive, computed, v-model, watch)'
248
+ },
249
+ {
250
+ category: 'vue-props-emits',
251
+ value: 'vue-props-emits',
252
+ name: 'vue-props-emits (Parent/Child Component Communication & Custom Events)'
253
+ },
254
+ {
255
+ category: 'vue-router',
256
+ value: 'vue-router',
257
+ name: 'vue-router (Vue Router Parent Layout & Nested Children)'
258
+ },
259
+ {
260
+ category: 'vue-pinia',
261
+ value: 'vue-pinia',
262
+ name: 'vue-pinia (Pinia State Store: State, Getters, Actions)'
263
+ },
264
+ {
265
+ category: 'vue-router-pinia',
266
+ value: 'vue-router-pinia',
267
+ name: 'vue-router-pinia (Router Multi-Page Views + Pinia Store)'
268
+ },
269
+ {
270
+ category: 'vue-router-pinia-props',
271
+ value: 'vue-router-pinia-props',
272
+ name: 'vue-router-pinia-props (Router + Pinia Store + Component Props/Emits)'
273
+ },
274
+ {
275
+ category: 'vue-fetch',
276
+ value: 'vue-fetch',
277
+ name: 'vue-fetch (Native Fetch API CRUD without Axios)'
278
+ },
279
+ {
280
+ category: 'vue-todo-pinia',
281
+ value: 'vue-todo-pinia',
282
+ name: 'vue-todo-pinia (Pinia Task & Todo Manager)'
283
+ },
284
+ {
285
+ category: 'vue-todo-router',
286
+ value: 'vue-todo-router',
287
+ name: 'vue-todo-router (Router Multi-View Task Application)'
288
+ },
289
+ {
290
+ category: 'vue-cart-router-pinia',
291
+ value: 'vue-cart-router-pinia',
292
+ name: 'vue-cart-router-pinia (E-Commerce Shopping Cart with Router & Pinia)'
293
+ },
294
+ {
295
+ category: 'vue-food-ordering',
296
+ value: 'vue-food-ordering',
297
+ name: 'vue-food-ordering (Exercise 7 — Online Food Ordering Application)'
298
+ },
299
+ {
300
+ category: 'vue-symposium-system',
301
+ value: 'vue-symposium-system',
302
+ name: 'vue-symposium-system (Exercise 6 — College Symposium Management System)'
303
+ },
304
+ {
305
+ category: 'vue-banking-app',
306
+ value: 'vue-banking-app',
307
+ name: 'vue-banking-app (Banking Portal & Transaction Ledger System)'
308
+ },
309
+ {
310
+ category: 'vue-employee-system',
311
+ value: 'vue-employee-system',
312
+ name: 'vue-employee-system (Employee HR, Payroll & Records System)'
313
+ },
314
+ {
315
+ category: 'vue-hospital-portal',
316
+ value: 'vue-hospital-portal',
317
+ name: 'vue-hospital-portal (Hospital OPD & Patient Vitals Portal)'
318
+ },
319
+ {
320
+ category: 'vue-library-system',
321
+ value: 'vue-library-system',
322
+ name: 'vue-library-system (Library Catalog & Book Circulation System)'
323
+ },
324
+
325
+ // Games (9)
326
+ {
327
+ category: 'games',
328
+ value: 'game-bingo',
329
+ name: 'game-bingo (Exercise 3 — Two-Player JavaScript Bingo Game)'
330
+ },
331
+ {
332
+ category: 'games',
333
+ value: 'game-rock-paper-scissors',
334
+ name: 'game-rock-paper-scissors (Player vs Computer Rock Paper Scissors — Vanilla JS)'
335
+ },
336
+ {
337
+ category: 'games',
338
+ value: 'game-tic-tac-toe',
339
+ name: 'game-tic-tac-toe (3x3 Turn Grid with Win Detection — Vanilla JS)'
340
+ },
341
+ {
342
+ category: 'games',
343
+ value: 'game-whack-a-mole',
344
+ name: 'game-whack-a-mole (3x3 Mole Whack with Timer & Score — Vanilla JS)'
345
+ },
346
+ {
347
+ category: 'games',
348
+ value: 'game-memory-cards',
349
+ name: 'game-memory-cards (3x4 Flashcard Matching Game — Vanilla JS)'
350
+ },
351
+ {
352
+ category: 'games',
353
+ value: 'vue-game-rps',
354
+ name: 'vue-game-rps (Rock Paper Scissors with Vue 3 + Pinia Scoreboard)'
355
+ },
356
+ {
357
+ category: 'games',
358
+ value: 'vue-game-tictactoe',
359
+ name: 'vue-game-tictactoe (Tic Tac Toe with Vue 3 + Pinia Move History)'
360
+ },
361
+ {
362
+ category: 'games',
363
+ value: 'vue-game-whackamole',
364
+ name: 'vue-game-whackamole (Whack-A-Mole with Vue 3 + Pinia Countdown)'
365
+ },
366
+ {
367
+ category: 'games',
368
+ value: 'vue-memory-game',
369
+ name: 'vue-memory-game (Card Matching Game with Vue 3 + Pinia Flips)'
370
+ },
371
+
372
+ // HTML / CSS (2)
373
+ {
374
+ category: 'html-css',
375
+ value: 'html-css-js',
376
+ name: 'html-css-js (Form Validation Regex, Required & Dynamic DOM)'
377
+ },
378
+ {
379
+ category: 'html-css',
380
+ value: 'html-css-styles',
381
+ name: 'html-css-styles (50 CSS Properties Showcase: Box Model, Flex, Grid, Animations)'
382
+ }
102
383
  ];
103
384
 
104
385
  const PAGE_SIZE = 12;
386
+ const templatesRoot = path.join(__dirname, '..', 'templates');
387
+
388
+ let currentView = 'MAIN'; // 'MAIN' | 'CATEGORY' | 'SEARCH' | 'ALL'
389
+ let activeCategory = null;
390
+ let searchQuery = '';
105
391
  let selectedIndex = 0;
106
392
  const selectedValues = new Set();
107
- const templatesRoot = path.join(__dirname, '..', 'templates');
108
393
  let isPrompting = false;
109
394
 
110
- function renderMenu() {
111
- console.clear();
112
- console.log('=== WAD LAB STARTER TEMPLATES ===');
113
- console.log('Use [UP / DOWN] arrow keys to move');
114
- console.log('Press [SPACE] to select / deselect (multi-select supported)');
115
- console.log('Press [ENTER] to confirm and proceed\n');
116
-
117
- const total = templates.length;
118
- let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
119
- if (start < 0) start = 0;
120
- if (start + PAGE_SIZE > total) {
121
- start = Math.max(0, total - PAGE_SIZE);
395
+ function getActiveItemList() {
396
+ if (currentView === 'MAIN') {
397
+ const list = categories.map((cat, idx) => {
398
+ const count = templates.filter(t => t.category === cat.id).length;
399
+ return {
400
+ type: 'category',
401
+ id: cat.id,
402
+ name: `[${idx + 1}] ${cat.icon} ${cat.title} (${count} templates)`,
403
+ desc: cat.desc
404
+ };
405
+ });
406
+ list.push({ type: 'action', id: 'search', name: '[S] 🔍 Search templates by keyword...', desc: 'Quickly find templates by typing name or topic' });
407
+ list.push({ type: 'action', id: 'all_view', name: '[L] 📋 View all 56 templates (grouped)', desc: 'Browse the full catalog with section headers' });
408
+ list.push({ type: 'action', id: 'unpack_all', name: '[A] 📦 Unpack all 56 templates in 1-click', desc: 'Copy all 56 templates into target folder' });
409
+ return list;
122
410
  }
123
- const end = Math.min(start + PAGE_SIZE, total);
124
411
 
125
- if (start > 0) {
126
- console.log(` ▲ (${start} more above)`);
127
- } else {
128
- console.log(' --- Top of list ---');
412
+ if (currentView === 'CATEGORY') {
413
+ return templates.filter(t => t.category === activeCategory.id);
129
414
  }
130
415
 
131
- for (let i = start; i < end; i++) {
132
- const t = templates[i];
133
- const isCursor = i === selectedIndex;
134
- const isChecked = selectedValues.has(t.value);
135
- const box = isChecked ? '[x]' : '[ ]';
136
- const prefix = isCursor ? '> ' : ' ';
137
- console.log(prefix + box + ' ' + t.name);
416
+ if (currentView === 'SEARCH') {
417
+ const q = searchQuery.toLowerCase().trim();
418
+ if (!q) return [];
419
+ return templates.filter(t =>
420
+ t.value.toLowerCase().includes(q) ||
421
+ t.name.toLowerCase().includes(q) ||
422
+ t.category.toLowerCase().includes(q)
423
+ );
138
424
  }
139
425
 
140
- if (end < total) {
141
- console.log(` ▼ (${total - end} more below)`);
142
- } else {
143
- console.log(' --- Bottom of list ---');
426
+ if (currentView === 'ALL') {
427
+ return templates;
428
+ }
429
+
430
+ return [];
431
+ }
432
+
433
+ function renderScreen() {
434
+ console.clear();
435
+
436
+ if (currentView === 'MAIN') {
437
+ console.log('========================================================');
438
+ console.log(' 🚀 WAD LAB STARTER TEMPLATES ');
439
+ console.log(' 56 Exam-Ready & Lab-Tested Templates ');
440
+ console.log('========================================================\n');
441
+ console.log('Navigate with [UP / DOWN] arrow keys');
442
+ console.log('Press [ENTER] or press [1-7] to enter a category');
443
+ console.log('Press [S] to Search, [A] to Unpack All, [Ctrl+C] to Exit\n');
444
+
445
+ const items = getActiveItemList();
446
+ items.forEach((item, idx) => {
447
+ if (item.id === 'search') {
448
+ console.log(' ----------------------------------------------------');
449
+ }
450
+ const isCursor = idx === selectedIndex;
451
+ const prefix = isCursor ? ' 👉 ' : ' ';
452
+ console.log(prefix + item.name);
453
+ });
454
+
455
+ const activeItem = items[selectedIndex];
456
+ if (activeItem && activeItem.desc) {
457
+ console.log('\n ℹ️ ' + activeItem.desc);
458
+ }
459
+ return;
460
+ }
461
+
462
+ if (currentView === 'CATEGORY') {
463
+ console.log('========================================================');
464
+ console.log(` ${activeCategory.icon} ${activeCategory.title}`);
465
+ console.log(` ${activeCategory.desc}`);
466
+ console.log('========================================================\n');
467
+ console.log(' [SPACE] Select / Deselect (multi-select supported)');
468
+ console.log(' [ENTER] Confirm selection and proceed');
469
+ console.log(' [B / ESC] Back to Categories Menu\n');
470
+
471
+ const items = getActiveItemList();
472
+ const total = items.length;
473
+
474
+ let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
475
+ if (start < 0) start = 0;
476
+ if (start + PAGE_SIZE > total) {
477
+ start = Math.max(0, total - PAGE_SIZE);
478
+ }
479
+ const end = Math.min(start + PAGE_SIZE, total);
480
+
481
+ if (start > 0) {
482
+ console.log(` ▲ (${start} more above)`);
483
+ }
484
+
485
+ for (let i = start; i < end; i++) {
486
+ const t = items[i];
487
+ const isCursor = i === selectedIndex;
488
+ const isChecked = selectedValues.has(t.value);
489
+ const box = isChecked ? '[✓]' : '[ ]';
490
+ const prefix = isCursor ? ' 👉 ' : ' ';
491
+ console.log(prefix + box + ' ' + t.name);
492
+ }
493
+
494
+ if (end < total) {
495
+ console.log(` ▼ (${total - end} more below)`);
496
+ }
497
+
498
+ const selCount = selectedValues.size;
499
+ console.log(`\n Selected: ${selCount} template(s) [${selectedIndex + 1}/${total}]`);
500
+ return;
501
+ }
502
+
503
+ if (currentView === 'SEARCH') {
504
+ console.log('========================================================');
505
+ console.log(' 🔍 Instant Template Search');
506
+ console.log('========================================================');
507
+ console.log(` Query: "${searchQuery}" (type letters/numbers, Backspace to delete)`);
508
+ console.log(' [SPACE] Select | [ENTER] Confirm | [ESC] Back\n');
509
+
510
+ const items = getActiveItemList();
511
+ if (!searchQuery.trim()) {
512
+ console.log(' Start typing to search across all 56 templates...');
513
+ console.log(' Try keywords: "mongo", "join", "props", "servlet", "pinia", "game"\n');
514
+ return;
515
+ }
516
+
517
+ if (items.length === 0) {
518
+ console.log(` No templates matched "${searchQuery}".`);
519
+ return;
520
+ }
521
+
522
+ const total = items.length;
523
+ let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
524
+ if (start < 0) start = 0;
525
+ if (start + PAGE_SIZE > total) {
526
+ start = Math.max(0, total - PAGE_SIZE);
527
+ }
528
+ const end = Math.min(start + PAGE_SIZE, total);
529
+
530
+ if (start > 0) {
531
+ console.log(` ▲ (${start} more above)`);
532
+ }
533
+
534
+ for (let i = start; i < end; i++) {
535
+ const t = items[i];
536
+ const isCursor = i === selectedIndex;
537
+ const isChecked = selectedValues.has(t.value);
538
+ const box = isChecked ? '[✓]' : '[ ]';
539
+ const prefix = isCursor ? ' 👉 ' : ' ';
540
+ console.log(prefix + box + ' ' + t.name);
541
+ }
542
+
543
+ if (end < total) {
544
+ console.log(` ▼ (${total - end} more below)`);
545
+ }
546
+
547
+ console.log(`\n Matches: ${total} | Selected: ${selectedValues.size}`);
548
+ return;
144
549
  }
145
550
 
146
- const selectedDisplay = selectedValues.size === 0
147
- ? templates[selectedIndex].name
148
- : `${selectedValues.size} template(s) selected: ${Array.from(selectedValues).join(', ')}`;
149
- console.log(`\n[${selectedIndex + 1}/${total}] Focused: ${templates[selectedIndex].value}`);
150
- console.log(`Selection: ${selectedDisplay}`);
151
- console.log('\nPress Ctrl+C to cancel.');
551
+ if (currentView === 'ALL') {
552
+ console.log('========================================================');
553
+ console.log(' 📋 Full Catalog: All 56 Templates');
554
+ console.log('========================================================\n');
555
+ console.log(' [SPACE] Select / Deselect | [ENTER] Confirm | [ESC] Back\n');
556
+
557
+ const items = getActiveItemList();
558
+ const total = items.length;
559
+
560
+ let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
561
+ if (start < 0) start = 0;
562
+ if (start + PAGE_SIZE > total) {
563
+ start = Math.max(0, total - PAGE_SIZE);
564
+ }
565
+ const end = Math.min(start + PAGE_SIZE, total);
566
+
567
+ if (start > 0) {
568
+ console.log(` ▲ (${start} more above)`);
569
+ }
570
+
571
+ let lastCat = null;
572
+ for (let i = start; i < end; i++) {
573
+ const t = items[i];
574
+ if (t.category !== lastCat) {
575
+ const catObj = categories.find(c => c.id === t.category);
576
+ if (catObj) {
577
+ console.log(` --- ${catObj.icon} ${catObj.title} ---`);
578
+ }
579
+ lastCat = t.category;
580
+ }
581
+ const isCursor = i === selectedIndex;
582
+ const isChecked = selectedValues.has(t.value);
583
+ const box = isChecked ? '[✓]' : '[ ]';
584
+ const prefix = isCursor ? ' 👉 ' : ' ';
585
+ console.log(prefix + box + ' ' + t.name);
586
+ }
587
+
588
+ if (end < total) {
589
+ console.log(` ▼ (${total - end} more below)`);
590
+ }
591
+
592
+ console.log(`\n [${selectedIndex + 1}/${total}] Selected: ${selectedValues.size}`);
593
+ }
152
594
  }
153
595
 
154
596
  function promptFolderName(choices) {
@@ -208,7 +650,7 @@ function promptFolderName(choices) {
208
650
 
209
651
  if (isAll) {
210
652
  copyDirRecursive(templatesRoot, destDir);
211
- console.log('\nSuccessfully unpacked ALL templates into: ' + destDir);
653
+ console.log('\n✨ Successfully unpacked ALL 56 templates into: ' + destDir);
212
654
  } else {
213
655
  choices.forEach(c => {
214
656
  if (c === 'fullstack-springboot-vue') {
@@ -238,29 +680,56 @@ function promptFolderName(choices) {
238
680
  console.log('Unpacked [' + c + '] into: ' + dest);
239
681
  }
240
682
  });
241
- console.log('\nSuccessfully unpacked all chosen templates into: ' + destDir);
683
+ console.log('\n✨ Successfully unpacked all chosen templates into: ' + destDir);
242
684
  }
243
685
  process.exit(0);
244
686
  });
245
687
  });
246
688
  }
247
689
 
690
+ // Check command-line arguments for direct non-interactive use
248
691
  if (process.argv[2]) {
249
- const arg = process.argv[2];
250
- const found = templates.find(t => t.value === arg);
251
- if (found) {
252
- promptFolderName([found.value]);
692
+ const arg = process.argv[2].trim();
693
+ if (arg === 'all' || arg === '--all' || arg === '-a') {
694
+ promptFolderName(['all']);
695
+ } else if (arg === '--cat' && process.argv[3]) {
696
+ const catId = process.argv[3].trim().toLowerCase();
697
+ const foundCat = categories.find(c => c.id === catId);
698
+ if (foundCat) {
699
+ activeCategory = foundCat;
700
+ currentView = 'CATEGORY';
701
+ selectedIndex = 0;
702
+ startInteractive();
703
+ } else {
704
+ console.error(`Unknown category: "${catId}". Available: ${categories.map(c => c.id).join(', ')}`);
705
+ process.exit(1);
706
+ }
707
+ } else if (arg === '--search' && process.argv[3]) {
708
+ searchQuery = process.argv.slice(3).join(' ');
709
+ currentView = 'SEARCH';
710
+ selectedIndex = 0;
711
+ startInteractive();
253
712
  } else {
254
- console.error('Unknown template: ' + arg);
255
- process.exit(1);
713
+ const found = templates.find(t => t.value === arg);
714
+ if (found) {
715
+ promptFolderName([found.value]);
716
+ } else {
717
+ console.error('Unknown template: ' + arg);
718
+ console.error(`Available templates (56 total) or run without arguments for the interactive menu.`);
719
+ process.exit(1);
720
+ }
256
721
  }
257
722
  } else {
723
+ startInteractive();
724
+ }
725
+
726
+ function startInteractive() {
258
727
  readline.emitKeypressEvents(process.stdin);
259
728
  if (process.stdin.isTTY) {
260
729
  process.stdin.setRawMode(true);
261
730
  }
262
731
 
263
- renderMenu();
732
+ renderScreen();
264
733
 
265
734
  process.stdin.on('keypress', (str, key) => {
266
735
  if (isPrompting) return;
@@ -269,36 +738,175 @@ if (process.argv[2]) {
269
738
  process.exit(0);
270
739
  }
271
740
 
272
- if (key.name === 'up') {
273
- selectedIndex = (selectedIndex - 1 + templates.length) % templates.length;
274
- renderMenu();
275
- } else if (key.name === 'down') {
276
- selectedIndex = (selectedIndex + 1) % templates.length;
277
- renderMenu();
278
- } else if (key.name === 'space') {
279
- const currentVal = templates[selectedIndex].value;
280
- if (currentVal === 'all') {
281
- if (selectedValues.has('all')) {
282
- selectedValues.clear();
283
- } else {
284
- selectedValues.clear();
285
- selectedValues.add('all');
741
+ const items = getActiveItemList();
742
+
743
+ // MAIN VIEW INPUT
744
+ if (currentView === 'MAIN') {
745
+ if (key.name === 'up') {
746
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
747
+ renderScreen();
748
+ } else if (key.name === 'down') {
749
+ selectedIndex = (selectedIndex + 1) % items.length;
750
+ renderScreen();
751
+ } else if (str === 's' || str === 'S') {
752
+ currentView = 'SEARCH';
753
+ searchQuery = '';
754
+ selectedIndex = 0;
755
+ renderScreen();
756
+ } else if (str === 'a' || str === 'A') {
757
+ promptFolderName(['all']);
758
+ } else if (str === 'l' || str === 'L') {
759
+ currentView = 'ALL';
760
+ selectedIndex = 0;
761
+ renderScreen();
762
+ } else if (str >= '1' && str <= '7') {
763
+ const num = parseInt(str, 10);
764
+ activeCategory = categories[num - 1];
765
+ currentView = 'CATEGORY';
766
+ selectedIndex = 0;
767
+ renderScreen();
768
+ } else if (key.name === 'return' || key.name === 'enter') {
769
+ const item = items[selectedIndex];
770
+ if (item.type === 'category') {
771
+ activeCategory = categories.find(c => c.id === item.id);
772
+ currentView = 'CATEGORY';
773
+ selectedIndex = 0;
774
+ renderScreen();
775
+ } else if (item.id === 'search') {
776
+ currentView = 'SEARCH';
777
+ searchQuery = '';
778
+ selectedIndex = 0;
779
+ renderScreen();
780
+ } else if (item.id === 'all_view') {
781
+ currentView = 'ALL';
782
+ selectedIndex = 0;
783
+ renderScreen();
784
+ } else if (item.id === 'unpack_all') {
785
+ promptFolderName(['all']);
286
786
  }
287
- } else {
288
- selectedValues.delete('all');
289
- if (selectedValues.has(currentVal)) {
290
- selectedValues.delete(currentVal);
787
+ }
788
+ return;
789
+ }
790
+
791
+ // SEARCH VIEW INPUT
792
+ if (currentView === 'SEARCH') {
793
+ if (key.name === 'escape') {
794
+ currentView = 'MAIN';
795
+ selectedIndex = 0;
796
+ renderScreen();
797
+ return;
798
+ }
799
+
800
+ if (key.name === 'up') {
801
+ if (items.length > 0) {
802
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
803
+ renderScreen();
804
+ }
805
+ return;
806
+ }
807
+
808
+ if (key.name === 'down') {
809
+ if (items.length > 0) {
810
+ selectedIndex = (selectedIndex + 1) % items.length;
811
+ renderScreen();
812
+ }
813
+ return;
814
+ }
815
+
816
+ if (key.name === 'space') {
817
+ if (items.length > 0) {
818
+ const currentVal = items[selectedIndex].value;
819
+ if (selectedValues.has(currentVal)) {
820
+ selectedValues.delete(currentVal);
821
+ } else {
822
+ selectedValues.add(currentVal);
823
+ }
824
+ renderScreen();
825
+ }
826
+ return;
827
+ }
828
+
829
+ if (key.name === 'return' || key.name === 'enter') {
830
+ let chosen = Array.from(selectedValues);
831
+ if (chosen.length === 0 && items.length > 0) {
832
+ chosen = [items[selectedIndex].value];
833
+ }
834
+ if (chosen.length > 0) {
835
+ promptFolderName(chosen);
836
+ }
837
+ return;
838
+ }
839
+
840
+ if (key.name === 'backspace') {
841
+ if (searchQuery.length > 0) {
842
+ searchQuery = searchQuery.slice(0, -1);
843
+ selectedIndex = 0;
844
+ renderScreen();
291
845
  } else {
292
- selectedValues.add(currentVal);
846
+ currentView = 'MAIN';
847
+ selectedIndex = 0;
848
+ renderScreen();
849
+ }
850
+ return;
851
+ }
852
+
853
+ // Typing search query
854
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
855
+ searchQuery += str;
856
+ selectedIndex = 0;
857
+ renderScreen();
858
+ return;
859
+ }
860
+ return;
861
+ }
862
+
863
+ // CATEGORY / ALL VIEW INPUT
864
+ if (currentView === 'CATEGORY' || currentView === 'ALL') {
865
+ if (key.name === 'escape' || key.name === 'left' || str === 'b' || str === 'B') {
866
+ currentView = 'MAIN';
867
+ selectedIndex = 0;
868
+ renderScreen();
869
+ return;
870
+ }
871
+
872
+ if (key.name === 'up') {
873
+ if (items.length > 0) {
874
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
875
+ renderScreen();
876
+ }
877
+ return;
878
+ }
879
+
880
+ if (key.name === 'down') {
881
+ if (items.length > 0) {
882
+ selectedIndex = (selectedIndex + 1) % items.length;
883
+ renderScreen();
293
884
  }
885
+ return;
294
886
  }
295
- renderMenu();
296
- } else if (key.name === 'return' || key.name === 'enter') {
297
- let chosen = Array.from(selectedValues);
298
- if (chosen.length === 0) {
299
- chosen = [templates[selectedIndex].value];
887
+
888
+ if (key.name === 'space') {
889
+ if (items.length > 0) {
890
+ const currentVal = items[selectedIndex].value;
891
+ if (selectedValues.has(currentVal)) {
892
+ selectedValues.delete(currentVal);
893
+ } else {
894
+ selectedValues.add(currentVal);
895
+ }
896
+ renderScreen();
897
+ }
898
+ return;
899
+ }
900
+
901
+ if (key.name === 'return' || key.name === 'enter') {
902
+ let chosen = Array.from(selectedValues);
903
+ if (chosen.length === 0 && items.length > 0) {
904
+ chosen = [items[selectedIndex].value];
905
+ }
906
+ if (chosen.length > 0) {
907
+ promptFolderName(chosen);
908
+ }
300
909
  }
301
- promptFolderName(chosen);
302
910
  }
303
911
  });
304
912
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wad-lab",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Interactive CLI for WAD Lab starter templates compatible with Node 12+",
5
5
  "bin": {
6
6
  "wad-lab": "bin/index.js"