wad-lab 1.2.0 → 1.2.2

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/index.js CHANGED
@@ -41,114 +41,630 @@ const frontendOptions = [
41
41
  { name: 'vue-axios-interceptor (Vue 3 + Axios Interceptors + Toast Alerts)', value: 'vue-axios-interceptor' }
42
42
  ];
43
43
 
44
+ const featuredTemplates = [
45
+ {
46
+ key: '1',
47
+ value: 'fullstack-springboot-vue',
48
+ name: '[1] fullstack-springboot-vue (Spring Boot 2-Model API + Selectable Vue Frontend)',
49
+ desc: 'Complete stack: 2-model Spring Boot backend + matching Vue frontend (Router/Pinia/Props)'
50
+ },
51
+ {
52
+ key: '2',
53
+ value: 'springboot-2model-1controller-api',
54
+ name: '[2] springboot-2model-1controller-api (2 Models Dept & Emp, 2 Repos, 1 Service)',
55
+ desc: 'Extensible 2-model Spring Boot architecture (Department + Employee) with full CRUD & PATCH'
56
+ },
57
+ {
58
+ key: '3',
59
+ value: 'springboot-mongo-aggregation-join',
60
+ name: '[3] springboot-mongo-aggregation-join ($lookup JOIN, @Query, @Aggregation)',
61
+ desc: 'MongoDB $lookup multi-collection JOIN, pipeline stages ($match/$group), and @Query filters'
62
+ },
63
+ {
64
+ key: '4',
65
+ value: 'vue-axios-router-pinia-props',
66
+ name: '[4] vue-axios-router-pinia-props (Matching Frontend: Router + Pinia + Props/Emits)',
67
+ desc: 'Matching frontend for 2-model backend: Vue Router + Pinia centralized store + Props/Emits'
68
+ },
69
+ {
70
+ key: '5',
71
+ value: 'vue-axios-router-props',
72
+ name: '[5] vue-axios-router-props (Matching Frontend: Router Views + Props/Emits)',
73
+ desc: 'Matching frontend for 2-model backend: Vue Router pages + reusable components with Props/Emits'
74
+ },
75
+ {
76
+ key: '6',
77
+ value: 'servlet-jdbc',
78
+ name: '[6] servlet-jdbc (Tomcat 9 + MySQL Database Connection via JDBC)',
79
+ desc: 'Tomcat Servlet with MySQL Driver, JDBC connection, SQL statement execution, and HTML table'
80
+ },
81
+ {
82
+ key: '7',
83
+ value: 'servlet-flow',
84
+ name: '[7] servlet-flow (Tomcat 9 Execution Flow with GET/POST Form — No DB)',
85
+ desc: 'Pure servlet without DB: HTML form submission, doGet / doPost processing, and response render'
86
+ }
87
+ ];
88
+
89
+ const categories = [
90
+ {
91
+ id: 'fullstack',
92
+ title: 'Fullstack Web Applications',
93
+ desc: 'Integrated Spring Boot + MongoDB + Vue 3 apps'
94
+ },
95
+ {
96
+ id: 'springboot',
97
+ title: 'Spring Boot Backend APIs',
98
+ desc: 'REST APIs, MongoDB, Aggregations, Join & MockMvc'
99
+ },
100
+ {
101
+ id: 'servlet',
102
+ title: 'Java Servlets & Tomcat 9',
103
+ desc: 'Classic Tomcat Servlets, JDBC, Sessions & Web.xml'
104
+ },
105
+ {
106
+ id: 'vue-axios',
107
+ title: 'Vue 3 + Axios HTTP Variants',
108
+ desc: 'All 10+ architectural combinations of Vue 3 with Axios'
109
+ },
110
+ {
111
+ id: 'vue-core',
112
+ title: 'Vue 3 Core Portals & State',
113
+ desc: 'Router, Pinia, Native Fetch & Complete UI Portals'
114
+ },
115
+ {
116
+ id: 'games',
117
+ title: 'Interactive Games (JS & Vue)',
118
+ desc: 'Logic, DOM & State Management Games'
119
+ },
120
+ {
121
+ id: 'html-css',
122
+ title: 'HTML5 & CSS3 Foundations',
123
+ desc: 'Form Validation, DOM Manipulation & 50 CSS Properties'
124
+ }
125
+ ];
126
+
44
127
  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' }
128
+ // Fullstack (4)
129
+ {
130
+ category: 'fullstack',
131
+ value: 'fullstack-springboot-vue',
132
+ name: 'fullstack-springboot-vue (Spring Boot 2-Model API + Selectable Vue 3 Frontend)'
133
+ },
134
+ {
135
+ category: 'fullstack',
136
+ value: 'vue-springboot-mongo-crud',
137
+ name: 'vue-springboot-mongo-crud (Spring Boot + MongoDB + Vue 3 Direct CRUD)'
138
+ },
139
+ {
140
+ category: 'fullstack',
141
+ value: 'vue-springboot-mongo-aggregation',
142
+ name: 'vue-springboot-mongo-aggregation (Spring Boot Aggregation + Vue 3 Dashboard)'
143
+ },
144
+ {
145
+ category: 'fullstack',
146
+ value: 'vue-router-pinia-springboot',
147
+ name: 'vue-router-pinia-springboot (Fullstack Enterprise: Router + Pinia + Spring Boot)'
148
+ },
149
+
150
+ // Spring Boot (9)
151
+ {
152
+ category: 'springboot',
153
+ value: 'springboot-mongo-crud',
154
+ name: 'springboot-mongo-crud (4-Layer REST API: Controller -> Service -> Repo -> Mongo)'
155
+ },
156
+ {
157
+ category: 'springboot',
158
+ value: 'springboot-mongo-aggregation',
159
+ name: 'springboot-mongo-aggregation (Aggregation Pipeline: $group, $sum, $avg, $sort)'
160
+ },
161
+ {
162
+ category: 'springboot',
163
+ value: 'springboot-mongo-aggregation-join',
164
+ name: 'springboot-mongo-aggregation-join (MongoDB $lookup JOIN, @Query, @Aggregation)'
165
+ },
166
+ {
167
+ category: 'springboot',
168
+ value: 'springboot-mockmvc',
169
+ name: 'springboot-mockmvc (Controller Unit Testing with MockMvc)'
170
+ },
171
+ {
172
+ category: 'springboot',
173
+ value: 'springboot-mongo-aggregation-mockmvc',
174
+ name: 'springboot-mongo-aggregation-mockmvc (Full Testing Lab: MockMvc + Aggregation)'
175
+ },
176
+ {
177
+ category: 'springboot',
178
+ value: 'springboot-2model-1controller-api',
179
+ name: 'springboot-2model-1controller-api (2 Models Dept & Emp, 2 Repos, 1 Service, 1 Controller)'
180
+ },
181
+ {
182
+ category: 'springboot',
183
+ value: 'springboot-course-registration',
184
+ name: 'springboot-course-registration (Exercise 5 — Course Registration REST API)'
185
+ },
186
+ {
187
+ category: 'springboot',
188
+ value: 'springboot-hospital-api',
189
+ name: 'springboot-hospital-api (Hospital OPD Appointments & Doctors API)'
190
+ },
191
+ {
192
+ category: 'springboot',
193
+ value: 'springboot-library-api',
194
+ name: 'springboot-library-api (Library Catalog, Loans & Analytics API)'
195
+ },
196
+
197
+ // Servlets (5)
198
+ {
199
+ category: 'servlet',
200
+ value: 'servlet-basic',
201
+ name: 'servlet-basic (Tomcat GET + POST Basic Servlet)'
202
+ },
203
+ {
204
+ category: 'servlet',
205
+ value: 'servlet-flow',
206
+ name: 'servlet-flow (HelloServlet Execution Flow with GET/POST Form)'
207
+ },
208
+ {
209
+ category: 'servlet',
210
+ value: 'servlet-jdbc',
211
+ name: 'servlet-jdbc (Tomcat + MySQL JDBC Database Connection)'
212
+ },
213
+ {
214
+ category: 'servlet',
215
+ value: 'servlet-session-tracking',
216
+ name: 'servlet-session-tracking (HttpSession, Cookies, Redirect & URL Rewriting)'
217
+ },
218
+ {
219
+ category: 'servlet',
220
+ value: 'servlet-voter-eligibility',
221
+ name: 'servlet-voter-eligibility (Voting Eligibility Check: Name & Age Validation)'
222
+ },
223
+
224
+ // Vue + Axios (11)
225
+ {
226
+ category: 'vue-axios',
227
+ value: 'vue-axios-basic',
228
+ name: 'vue-axios-basic (Vue 3 + Axios Direct CRUD in Single View)'
229
+ },
230
+ {
231
+ category: 'vue-axios-props-emits',
232
+ value: 'vue-axios-props-emits',
233
+ name: 'vue-axios-props-emits (Vue 3 + Axios + Parent/Child Props & Emits)'
234
+ },
235
+ {
236
+ category: 'vue-axios-router',
237
+ value: 'vue-axios-router',
238
+ name: 'vue-axios-router (Vue 3 + Axios + Router Views Dashboard & Details)'
239
+ },
240
+ {
241
+ category: 'vue-axios-pinia',
242
+ value: 'vue-axios-pinia',
243
+ name: 'vue-axios-pinia (Vue 3 + Axios + Pinia Centralized State Store)'
244
+ },
245
+ {
246
+ category: 'vue-axios-router-props',
247
+ value: 'vue-axios-router-props',
248
+ name: 'vue-axios-router-props (Vue 3 + Axios + Router + Component Props/Emits)'
249
+ },
250
+ {
251
+ category: 'vue-axios-pinia-props',
252
+ value: 'vue-axios-pinia-props',
253
+ name: 'vue-axios-pinia-props (Vue 3 + Axios + Pinia Store + Props/Emits)'
254
+ },
255
+ {
256
+ category: 'vue-axios-router-pinia',
257
+ value: 'vue-axios-router-pinia',
258
+ name: 'vue-axios-router-pinia (Vue 3 + Axios + Router Views + Pinia Store)'
259
+ },
260
+ {
261
+ category: 'vue-axios-router-pinia-props',
262
+ value: 'vue-axios-router-pinia-props',
263
+ name: 'vue-axios-router-pinia-props (The Complete Stack: Router + Pinia + Props + Emits)'
264
+ },
265
+ {
266
+ category: 'vue-axios-router-children',
267
+ value: 'vue-axios-router-children',
268
+ name: 'vue-axios-router-children (Vue 3 + Axios + Router Nested Children Layouts)'
269
+ },
270
+ {
271
+ category: 'vue-axios-interceptor',
272
+ value: 'vue-axios-interceptor',
273
+ name: 'vue-axios-interceptor (Vue 3 + Axios Interceptors + Toast Notifications)'
274
+ },
275
+ {
276
+ category: 'vue-axios',
277
+ value: 'vue-axios',
278
+ name: 'vue-axios (Classic Vue 3 Axios Service Layer CRUD)'
279
+ },
280
+
281
+ // Vue Core & Portals (16)
282
+ {
283
+ category: 'vue-core',
284
+ value: 'vue-basics',
285
+ name: 'vue-basics (Core Reactivity: ref, reactive, computed, v-model, watch)'
286
+ },
287
+ {
288
+ category: 'vue-props-emits',
289
+ value: 'vue-props-emits',
290
+ name: 'vue-props-emits (Parent/Child Component Communication & Custom Events)'
291
+ },
292
+ {
293
+ category: 'vue-router',
294
+ value: 'vue-router',
295
+ name: 'vue-router (Vue Router Parent Layout & Nested Children)'
296
+ },
297
+ {
298
+ category: 'vue-pinia',
299
+ value: 'vue-pinia',
300
+ name: 'vue-pinia (Pinia State Store: State, Getters, Actions)'
301
+ },
302
+ {
303
+ category: 'vue-router-pinia',
304
+ value: 'vue-router-pinia',
305
+ name: 'vue-router-pinia (Router Multi-Page Views + Pinia Store)'
306
+ },
307
+ {
308
+ category: 'vue-router-pinia-props',
309
+ value: 'vue-router-pinia-props',
310
+ name: 'vue-router-pinia-props (Router + Pinia Store + Component Props/Emits)'
311
+ },
312
+ {
313
+ category: 'vue-fetch',
314
+ value: 'vue-fetch',
315
+ name: 'vue-fetch (Native Fetch API CRUD without Axios)'
316
+ },
317
+ {
318
+ category: 'vue-todo-pinia',
319
+ value: 'vue-todo-pinia',
320
+ name: 'vue-todo-pinia (Pinia Task & Todo Manager)'
321
+ },
322
+ {
323
+ category: 'vue-todo-router',
324
+ value: 'vue-todo-router',
325
+ name: 'vue-todo-router (Router Multi-View Task Application)'
326
+ },
327
+ {
328
+ category: 'vue-cart-router-pinia',
329
+ value: 'vue-cart-router-pinia',
330
+ name: 'vue-cart-router-pinia (E-Commerce Shopping Cart with Router & Pinia)'
331
+ },
332
+ {
333
+ category: 'vue-food-ordering',
334
+ value: 'vue-food-ordering',
335
+ name: 'vue-food-ordering (Exercise 7 — Online Food Ordering Application)'
336
+ },
337
+ {
338
+ category: 'vue-symposium-system',
339
+ value: 'vue-symposium-system',
340
+ name: 'vue-symposium-system (Exercise 6 — College Symposium Management System)'
341
+ },
342
+ {
343
+ category: 'vue-banking-app',
344
+ value: 'vue-banking-app',
345
+ name: 'vue-banking-app (Banking Portal & Transaction Ledger System)'
346
+ },
347
+ {
348
+ category: 'vue-employee-system',
349
+ value: 'vue-employee-system',
350
+ name: 'vue-employee-system (Employee HR, Payroll & Records System)'
351
+ },
352
+ {
353
+ category: 'vue-hospital-portal',
354
+ value: 'vue-hospital-portal',
355
+ name: 'vue-hospital-portal (Hospital OPD & Patient Vitals Portal)'
356
+ },
357
+ {
358
+ category: 'vue-library-system',
359
+ value: 'vue-library-system',
360
+ name: 'vue-library-system (Library Catalog & Book Circulation System)'
361
+ },
362
+
363
+ // Games (9)
364
+ {
365
+ category: 'games',
366
+ value: 'game-bingo',
367
+ name: 'game-bingo (Exercise 3 — Two-Player JavaScript Bingo Game)'
368
+ },
369
+ {
370
+ category: 'games',
371
+ value: 'game-rock-paper-scissors',
372
+ name: 'game-rock-paper-scissors (Player vs Computer Rock Paper Scissors — Vanilla JS)'
373
+ },
374
+ {
375
+ category: 'games',
376
+ value: 'game-tic-tac-toe',
377
+ name: 'game-tic-tac-toe (3x3 Turn Grid with Win Detection — Vanilla JS)'
378
+ },
379
+ {
380
+ category: 'games',
381
+ value: 'game-whack-a-mole',
382
+ name: 'game-whack-a-mole (3x3 Mole Whack with Timer & Score — Vanilla JS)'
383
+ },
384
+ {
385
+ category: 'games',
386
+ value: 'game-memory-cards',
387
+ name: 'game-memory-cards (3x4 Flashcard Matching Game — Vanilla JS)'
388
+ },
389
+ {
390
+ category: 'games',
391
+ value: 'vue-game-rps',
392
+ name: 'vue-game-rps (Rock Paper Scissors with Vue 3 + Pinia Scoreboard)'
393
+ },
394
+ {
395
+ category: 'games',
396
+ value: 'vue-game-tictactoe',
397
+ name: 'vue-game-tictactoe (Tic Tac Toe with Vue 3 + Pinia Move History)'
398
+ },
399
+ {
400
+ category: 'games',
401
+ value: 'vue-game-whackamole',
402
+ name: 'vue-game-whackamole (Whack-A-Mole with Vue 3 + Pinia Countdown)'
403
+ },
404
+ {
405
+ category: 'games',
406
+ value: 'vue-memory-game',
407
+ name: 'vue-memory-game (Card Matching Game with Vue 3 + Pinia Flips)'
408
+ },
409
+
410
+ // HTML / CSS (2)
411
+ {
412
+ category: 'html-css',
413
+ value: 'html-css-js',
414
+ name: 'html-css-js (Form Validation Regex, Required & Dynamic DOM)'
415
+ },
416
+ {
417
+ category: 'html-css',
418
+ value: 'html-css-styles',
419
+ name: 'html-css-styles (50 CSS Properties Showcase: Box Model, Flex, Grid, Animations)'
420
+ }
102
421
  ];
103
422
 
104
423
  const PAGE_SIZE = 12;
424
+ const templatesRoot = path.join(__dirname, '..', 'templates');
425
+
426
+ let currentView = 'MAIN'; // 'MAIN' | 'CATEGORY_LIST' | 'CATEGORY' | 'SEARCH' | 'ALL'
427
+ let activeCategory = null;
428
+ let searchQuery = '';
105
429
  let selectedIndex = 0;
106
430
  const selectedValues = new Set();
107
- const templatesRoot = path.join(__dirname, '..', 'templates');
108
431
  let isPrompting = false;
109
432
 
110
- function renderMenu() {
433
+ function getActiveItemList() {
434
+ if (currentView === 'MAIN') {
435
+ const list = featuredTemplates.map(f => ({
436
+ type: 'featured',
437
+ id: f.value,
438
+ name: f.name,
439
+ desc: f.desc
440
+ }));
441
+ list.push({ type: 'action', id: 'categories', name: '[C] Browse by Category (7 Groups, 56 Templates)', desc: 'Explore all 56 templates categorized into 7 groups' });
442
+ list.push({ type: 'action', id: 'search', name: '[S] Search by keyword across all 56 templates', desc: 'Type any keyword to find templates instantly' });
443
+ list.push({ type: 'action', id: 'all_view', name: '[L] View all 56 templates (grouped catalog)', desc: 'Browse the entire catalog with section dividers' });
444
+ list.push({ type: 'action', id: 'unpack_all', name: '[A] Unpack all 56 templates in 1-click', desc: 'Copy every single template into target folder' });
445
+ return list;
446
+ }
447
+
448
+ if (currentView === 'CATEGORY_LIST') {
449
+ return categories.map((cat, idx) => {
450
+ const count = templates.filter(t => t.category === cat.id).length;
451
+ return {
452
+ type: 'cat_entry',
453
+ id: cat.id,
454
+ name: `[${idx + 1}] ${cat.title} (${count} templates)`,
455
+ desc: cat.desc
456
+ };
457
+ });
458
+ }
459
+
460
+ if (currentView === 'CATEGORY') {
461
+ return templates.filter(t => t.category === activeCategory.id);
462
+ }
463
+
464
+ if (currentView === 'SEARCH') {
465
+ const q = searchQuery.toLowerCase().trim();
466
+ if (!q) return [];
467
+ return templates.filter(t =>
468
+ t.value.toLowerCase().includes(q) ||
469
+ t.name.toLowerCase().includes(q) ||
470
+ t.category.toLowerCase().includes(q)
471
+ );
472
+ }
473
+
474
+ if (currentView === 'ALL') {
475
+ return templates;
476
+ }
477
+
478
+ return [];
479
+ }
480
+
481
+ function renderScreen() {
111
482
  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);
483
+
484
+ if (currentView === 'MAIN') {
485
+ console.log('========================================================');
486
+ console.log(' WAD LAB STARTER TEMPLATES ');
487
+ console.log(' Exam Quick-Start & 56 Ready Templates ');
488
+ console.log('========================================================\n');
489
+ console.log('CORE EXAM STARTERS:');
490
+ console.log(' Press [1-7] for instant pick, or navigate with [UP / DOWN]');
491
+ console.log(' [SPACE] Select | [ENTER] Confirm | [C] Categories | [S] Search | [A] All\n');
492
+
493
+ const items = getActiveItemList();
494
+ items.forEach((item, idx) => {
495
+ if (item.id === 'categories') {
496
+ console.log('\nFULL CATALOG & EXPLORATION:');
497
+ }
498
+ const isCursor = idx === selectedIndex;
499
+ const isChecked = selectedValues.has(item.id);
500
+ const box = item.type === 'featured' ? (isChecked ? '[x] ' : '[ ] ') : '';
501
+ const prefix = isCursor ? ' > ' : ' ';
502
+ console.log(prefix + box + item.name);
503
+ });
504
+
505
+ const activeItem = items[selectedIndex];
506
+ if (activeItem && activeItem.desc) {
507
+ console.log('\n Info: ' + activeItem.desc);
508
+ }
509
+ if (selectedValues.size > 0) {
510
+ console.log(`\n Selected: ${selectedValues.size} template(s) [Press ENTER to proceed]`);
511
+ }
512
+ return;
122
513
  }
123
- const end = Math.min(start + PAGE_SIZE, total);
124
514
 
125
- if (start > 0) {
126
- console.log(` ▲ (${start} more above)`);
127
- } else {
128
- console.log(' --- Top of list ---');
515
+ if (currentView === 'CATEGORY_LIST') {
516
+ console.log('========================================================');
517
+ console.log(' Browse Templates by Category (56 Total)');
518
+ console.log('========================================================\n');
519
+ console.log(' Press [1-7] or [ENTER] to open category');
520
+ console.log(' [B / ESC] Back to Main Menu\n');
521
+
522
+ const items = getActiveItemList();
523
+ items.forEach((item, idx) => {
524
+ const isCursor = idx === selectedIndex;
525
+ const prefix = isCursor ? ' > ' : ' ';
526
+ console.log(prefix + item.name);
527
+ });
528
+
529
+ const activeItem = items[selectedIndex];
530
+ if (activeItem && activeItem.desc) {
531
+ console.log('\n Info: ' + activeItem.desc);
532
+ }
533
+ return;
129
534
  }
130
535
 
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);
536
+ if (currentView === 'CATEGORY') {
537
+ console.log('========================================================');
538
+ console.log(` ${activeCategory.title}`);
539
+ console.log(` ${activeCategory.desc}`);
540
+ console.log('========================================================\n');
541
+ console.log(' [SPACE] Select / Deselect (multi-select supported)');
542
+ console.log(' [ENTER] Confirm selection and proceed');
543
+ console.log(' [B / ESC] Back to Categories Menu\n');
544
+
545
+ const items = getActiveItemList();
546
+ const total = items.length;
547
+
548
+ let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
549
+ if (start < 0) start = 0;
550
+ if (start + PAGE_SIZE > total) {
551
+ start = Math.max(0, total - PAGE_SIZE);
552
+ }
553
+ const end = Math.min(start + PAGE_SIZE, total);
554
+
555
+ if (start > 0) {
556
+ console.log(` ^ (${start} more above)`);
557
+ }
558
+
559
+ for (let i = start; i < end; i++) {
560
+ const t = items[i];
561
+ const isCursor = i === selectedIndex;
562
+ const isChecked = selectedValues.has(t.value);
563
+ const box = isChecked ? '[x]' : '[ ]';
564
+ const prefix = isCursor ? ' > ' : ' ';
565
+ console.log(prefix + box + ' ' + t.name);
566
+ }
567
+
568
+ if (end < total) {
569
+ console.log(` v (${total - end} more below)`);
570
+ }
571
+
572
+ const selCount = selectedValues.size;
573
+ console.log(`\n Selected: ${selCount} template(s) [${selectedIndex + 1}/${total}]`);
574
+ return;
138
575
  }
139
576
 
140
- if (end < total) {
141
- console.log(` ▼ (${total - end} more below)`);
142
- } else {
143
- console.log(' --- Bottom of list ---');
577
+ if (currentView === 'SEARCH') {
578
+ console.log('========================================================');
579
+ console.log(' Instant Template Search');
580
+ console.log('========================================================');
581
+ console.log(` Query: "${searchQuery}" (type letters/numbers, Backspace to delete)`);
582
+ console.log(' [SPACE] Select | [ENTER] Confirm | [ESC] Back\n');
583
+
584
+ const items = getActiveItemList();
585
+ if (!searchQuery.trim()) {
586
+ console.log(' Start typing to search across all 56 templates...');
587
+ console.log(' Try keywords: "join", "2model", "props", "servlet", "pinia", "mongo"\n');
588
+ return;
589
+ }
590
+
591
+ if (items.length === 0) {
592
+ console.log(` No templates matched "${searchQuery}".`);
593
+ return;
594
+ }
595
+
596
+ const total = items.length;
597
+ let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
598
+ if (start < 0) start = 0;
599
+ if (start + PAGE_SIZE > total) {
600
+ start = Math.max(0, total - PAGE_SIZE);
601
+ }
602
+ const end = Math.min(start + PAGE_SIZE, total);
603
+
604
+ if (start > 0) {
605
+ console.log(` ^ (${start} more above)`);
606
+ }
607
+
608
+ for (let i = start; i < end; i++) {
609
+ const t = items[i];
610
+ const isCursor = i === selectedIndex;
611
+ const isChecked = selectedValues.has(t.value);
612
+ const box = isChecked ? '[x]' : '[ ]';
613
+ const prefix = isCursor ? ' > ' : ' ';
614
+ console.log(prefix + box + ' ' + t.name);
615
+ }
616
+
617
+ if (end < total) {
618
+ console.log(` v (${total - end} more below)`);
619
+ }
620
+
621
+ console.log(`\n Matches: ${total} | Selected: ${selectedValues.size}`);
622
+ return;
144
623
  }
145
624
 
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.');
625
+ if (currentView === 'ALL') {
626
+ console.log('========================================================');
627
+ console.log(' Full Catalog: All 56 Templates');
628
+ console.log('========================================================\n');
629
+ console.log(' [SPACE] Select / Deselect | [ENTER] Confirm | [ESC] Back\n');
630
+
631
+ const items = getActiveItemList();
632
+ const total = items.length;
633
+
634
+ let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
635
+ if (start < 0) start = 0;
636
+ if (start + PAGE_SIZE > total) {
637
+ start = Math.max(0, total - PAGE_SIZE);
638
+ }
639
+ const end = Math.min(start + PAGE_SIZE, total);
640
+
641
+ if (start > 0) {
642
+ console.log(` ^ (${start} more above)`);
643
+ }
644
+
645
+ let lastCat = null;
646
+ for (let i = start; i < end; i++) {
647
+ const t = items[i];
648
+ if (t.category !== lastCat) {
649
+ const catObj = categories.find(c => c.id === t.category);
650
+ if (catObj) {
651
+ console.log(` --- ${catObj.title} ---`);
652
+ }
653
+ lastCat = t.category;
654
+ }
655
+ const isCursor = i === selectedIndex;
656
+ const isChecked = selectedValues.has(t.value);
657
+ const box = isChecked ? '[x]' : '[ ]';
658
+ const prefix = isCursor ? ' > ' : ' ';
659
+ console.log(prefix + box + ' ' + t.name);
660
+ }
661
+
662
+ if (end < total) {
663
+ console.log(` v (${total - end} more below)`);
664
+ }
665
+
666
+ console.log(`\n [${selectedIndex + 1}/${total}] Selected: ${selectedValues.size}`);
667
+ }
152
668
  }
153
669
 
154
670
  function promptFolderName(choices) {
@@ -208,7 +724,7 @@ function promptFolderName(choices) {
208
724
 
209
725
  if (isAll) {
210
726
  copyDirRecursive(templatesRoot, destDir);
211
- console.log('\nSuccessfully unpacked ALL templates into: ' + destDir);
727
+ console.log('\nSuccessfully unpacked ALL 56 templates into: ' + destDir);
212
728
  } else {
213
729
  choices.forEach(c => {
214
730
  if (c === 'fullstack-springboot-vue') {
@@ -245,22 +761,49 @@ function promptFolderName(choices) {
245
761
  });
246
762
  }
247
763
 
764
+ // Check command-line arguments for direct non-interactive use
248
765
  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]);
766
+ const arg = process.argv[2].trim();
767
+ if (arg === 'all' || arg === '--all' || arg === '-a') {
768
+ promptFolderName(['all']);
769
+ } else if (arg === '--cat' && process.argv[3]) {
770
+ const catId = process.argv[3].trim().toLowerCase();
771
+ const foundCat = categories.find(c => c.id === catId);
772
+ if (foundCat) {
773
+ activeCategory = foundCat;
774
+ currentView = 'CATEGORY';
775
+ selectedIndex = 0;
776
+ startInteractive();
777
+ } else {
778
+ console.error(`Unknown category: "${catId}". Available: ${categories.map(c => c.id).join(', ')}`);
779
+ process.exit(1);
780
+ }
781
+ } else if (arg === '--search' && process.argv[3]) {
782
+ searchQuery = process.argv.slice(3).join(' ');
783
+ currentView = 'SEARCH';
784
+ selectedIndex = 0;
785
+ startInteractive();
253
786
  } else {
254
- console.error('Unknown template: ' + arg);
255
- process.exit(1);
787
+ const found = templates.find(t => t.value === arg);
788
+ if (found) {
789
+ promptFolderName([found.value]);
790
+ } else {
791
+ console.error('Unknown template: ' + arg);
792
+ console.error(`Available templates (56 total) or run without arguments for the interactive menu.`);
793
+ process.exit(1);
794
+ }
256
795
  }
257
796
  } else {
797
+ startInteractive();
798
+ }
799
+
800
+ function startInteractive() {
258
801
  readline.emitKeypressEvents(process.stdin);
259
802
  if (process.stdin.isTTY) {
260
803
  process.stdin.setRawMode(true);
261
804
  }
262
805
 
263
- renderMenu();
806
+ renderScreen();
264
807
 
265
808
  process.stdin.on('keypress', (str, key) => {
266
809
  if (isPrompting) return;
@@ -269,36 +812,224 @@ if (process.argv[2]) {
269
812
  process.exit(0);
270
813
  }
271
814
 
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');
815
+ const items = getActiveItemList();
816
+
817
+ // MAIN VIEW INPUT
818
+ if (currentView === 'MAIN') {
819
+ if (key.name === 'up') {
820
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
821
+ renderScreen();
822
+ } else if (key.name === 'down') {
823
+ selectedIndex = (selectedIndex + 1) % items.length;
824
+ renderScreen();
825
+ } else if (str === 'c' || str === 'C') {
826
+ currentView = 'CATEGORY_LIST';
827
+ selectedIndex = 0;
828
+ renderScreen();
829
+ } else if (str === 's' || str === 'S') {
830
+ currentView = 'SEARCH';
831
+ searchQuery = '';
832
+ selectedIndex = 0;
833
+ renderScreen();
834
+ } else if (str === 'a' || str === 'A') {
835
+ promptFolderName(['all']);
836
+ } else if (str === 'l' || str === 'L') {
837
+ currentView = 'ALL';
838
+ selectedIndex = 0;
839
+ renderScreen();
840
+ } else if (str >= '1' && str <= '7') {
841
+ const num = parseInt(str, 10);
842
+ const target = featuredTemplates[num - 1];
843
+ if (target) {
844
+ promptFolderName([target.value]);
286
845
  }
287
- } else {
288
- selectedValues.delete('all');
289
- if (selectedValues.has(currentVal)) {
290
- selectedValues.delete(currentVal);
846
+ } else if (key.name === 'space') {
847
+ const item = items[selectedIndex];
848
+ if (item.type === 'featured') {
849
+ if (selectedValues.has(item.id)) {
850
+ selectedValues.delete(item.id);
851
+ } else {
852
+ selectedValues.add(item.id);
853
+ }
854
+ renderScreen();
855
+ }
856
+ } else if (key.name === 'return' || key.name === 'enter') {
857
+ const item = items[selectedIndex];
858
+ if (selectedValues.size > 0) {
859
+ promptFolderName(Array.from(selectedValues));
860
+ return;
861
+ }
862
+ if (item.type === 'featured') {
863
+ promptFolderName([item.id]);
864
+ } else if (item.id === 'categories') {
865
+ currentView = 'CATEGORY_LIST';
866
+ selectedIndex = 0;
867
+ renderScreen();
868
+ } else if (item.id === 'search') {
869
+ currentView = 'SEARCH';
870
+ searchQuery = '';
871
+ selectedIndex = 0;
872
+ renderScreen();
873
+ } else if (item.id === 'all_view') {
874
+ currentView = 'ALL';
875
+ selectedIndex = 0;
876
+ renderScreen();
877
+ } else if (item.id === 'unpack_all') {
878
+ promptFolderName(['all']);
879
+ }
880
+ }
881
+ return;
882
+ }
883
+
884
+ // CATEGORY_LIST VIEW INPUT
885
+ if (currentView === 'CATEGORY_LIST') {
886
+ if (key.name === 'escape' || key.name === 'left' || str === 'b' || str === 'B') {
887
+ currentView = 'MAIN';
888
+ selectedIndex = 0;
889
+ renderScreen();
890
+ return;
891
+ }
892
+ if (key.name === 'up') {
893
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
894
+ renderScreen();
895
+ } else if (key.name === 'down') {
896
+ selectedIndex = (selectedIndex + 1) % items.length;
897
+ renderScreen();
898
+ } else if (str >= '1' && str <= '7') {
899
+ const num = parseInt(str, 10);
900
+ activeCategory = categories[num - 1];
901
+ currentView = 'CATEGORY';
902
+ selectedIndex = 0;
903
+ renderScreen();
904
+ } else if (key.name === 'return' || key.name === 'enter') {
905
+ const item = items[selectedIndex];
906
+ activeCategory = categories.find(c => c.id === item.id);
907
+ currentView = 'CATEGORY';
908
+ selectedIndex = 0;
909
+ renderScreen();
910
+ }
911
+ return;
912
+ }
913
+
914
+ // SEARCH VIEW INPUT
915
+ if (currentView === 'SEARCH') {
916
+ if (key.name === 'escape') {
917
+ currentView = 'MAIN';
918
+ selectedIndex = 0;
919
+ renderScreen();
920
+ return;
921
+ }
922
+
923
+ if (key.name === 'up') {
924
+ if (items.length > 0) {
925
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
926
+ renderScreen();
927
+ }
928
+ return;
929
+ }
930
+
931
+ if (key.name === 'down') {
932
+ if (items.length > 0) {
933
+ selectedIndex = (selectedIndex + 1) % items.length;
934
+ renderScreen();
935
+ }
936
+ return;
937
+ }
938
+
939
+ if (key.name === 'space') {
940
+ if (items.length > 0) {
941
+ const currentVal = items[selectedIndex].value;
942
+ if (selectedValues.has(currentVal)) {
943
+ selectedValues.delete(currentVal);
944
+ } else {
945
+ selectedValues.add(currentVal);
946
+ }
947
+ renderScreen();
948
+ }
949
+ return;
950
+ }
951
+
952
+ if (key.name === 'return' || key.name === 'enter') {
953
+ let chosen = Array.from(selectedValues);
954
+ if (chosen.length === 0 && items.length > 0) {
955
+ chosen = [items[selectedIndex].value];
956
+ }
957
+ if (chosen.length > 0) {
958
+ promptFolderName(chosen);
959
+ }
960
+ return;
961
+ }
962
+
963
+ if (key.name === 'backspace') {
964
+ if (searchQuery.length > 0) {
965
+ searchQuery = searchQuery.slice(0, -1);
966
+ selectedIndex = 0;
967
+ renderScreen();
291
968
  } else {
292
- selectedValues.add(currentVal);
969
+ currentView = 'MAIN';
970
+ selectedIndex = 0;
971
+ renderScreen();
293
972
  }
973
+ return;
294
974
  }
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];
975
+
976
+ // Typing search query
977
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
978
+ searchQuery += str;
979
+ selectedIndex = 0;
980
+ renderScreen();
981
+ return;
982
+ }
983
+ return;
984
+ }
985
+
986
+ // CATEGORY / ALL VIEW INPUT
987
+ if (currentView === 'CATEGORY' || currentView === 'ALL') {
988
+ if (key.name === 'escape' || key.name === 'left' || str === 'b' || str === 'B') {
989
+ currentView = currentView === 'CATEGORY' ? 'CATEGORY_LIST' : 'MAIN';
990
+ selectedIndex = 0;
991
+ renderScreen();
992
+ return;
993
+ }
994
+
995
+ if (key.name === 'up') {
996
+ if (items.length > 0) {
997
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
998
+ renderScreen();
999
+ }
1000
+ return;
1001
+ }
1002
+
1003
+ if (key.name === 'down') {
1004
+ if (items.length > 0) {
1005
+ selectedIndex = (selectedIndex + 1) % items.length;
1006
+ renderScreen();
1007
+ }
1008
+ return;
1009
+ }
1010
+
1011
+ if (key.name === 'space') {
1012
+ if (items.length > 0) {
1013
+ const currentVal = items[selectedIndex].value;
1014
+ if (selectedValues.has(currentVal)) {
1015
+ selectedValues.delete(currentVal);
1016
+ } else {
1017
+ selectedValues.add(currentVal);
1018
+ }
1019
+ renderScreen();
1020
+ }
1021
+ return;
1022
+ }
1023
+
1024
+ if (key.name === 'return' || key.name === 'enter') {
1025
+ let chosen = Array.from(selectedValues);
1026
+ if (chosen.length === 0 && items.length > 0) {
1027
+ chosen = [items[selectedIndex].value];
1028
+ }
1029
+ if (chosen.length > 0) {
1030
+ promptFolderName(chosen);
1031
+ }
300
1032
  }
301
- promptFolderName(chosen);
302
1033
  }
303
1034
  });
304
1035
  }