npms-exam-kit 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/exam-kit.js +657 -278
  2. package/package.json +1 -3
package/bin/exam-kit.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from 'module';
3
2
  import { fileURLToPath } from 'url';
4
3
  import { dirname, join } from 'path';
5
4
  import fs from 'fs-extra';
@@ -13,339 +12,719 @@ const __filename = fileURLToPath(import.meta.url);
13
12
  const __dirname = dirname(__filename);
14
13
  const ROOT = join(__dirname, '..');
15
14
  const PROJECTS_DIR = join(ROOT, 'projects');
16
-
17
15
  const execAsync = promisify(exec);
18
16
 
19
- function log(msg) { console.log(msg); }
20
-
21
- const CHECKLIST = [
22
- { id: 'C1', cat: 'Preliminary (15%)', label: 'ERD Spare_Part entity drawn', auto: false },
23
- { id: 'C2', cat: 'Preliminary (15%)', label: 'ERD Stock_In entity drawn', auto: false },
24
- { id: 'C3', cat: 'Preliminary (15%)', label: 'ERD Stock_Out entity drawn', auto: false },
25
- { id: 'C4', cat: 'Preliminary (15%)', label: 'ERD entity symbol used', auto: false },
26
- { id: 'C5', cat: 'Preliminary (15%)', label: 'ERD relationship symbol used', auto: false },
27
- { id: 'C6', cat: 'Preliminary (15%)', label: 'ERD link symbol used', auto: false },
28
- { id: 'C7', cat: 'Preliminary (15%)', label: 'ERD Primary Key rule respected', auto: false },
29
- { id: 'C8', cat: 'Preliminary (15%)', label: 'ERD Foreign Key rule respected', auto: false },
30
- { id: 'C9', cat: 'Preliminary (15%)', label: 'ERD cardinalities indicated', auto: false },
31
- { id: 'C10', cat: 'Preliminary (15%)', label: 'Spare_Part-Stock_In relationship drawn', auto: false },
32
- { id: 'C11', cat: 'Preliminary (15%)', label: 'Users-Stock_Out relationship drawn', auto: false },
33
- { id: 'C12', cat: 'Preliminary (15%)', label: 'Spare_Part-Stock_Out relationship drawn', auto: false },
34
- { id: 'C13', cat: 'Preliminary (15%)', label: 'Users PK indicated in ERD', auto: false },
35
- { id: 'C14', cat: 'Preliminary (15%)', label: 'Spare_Part PK indicated in ERD', auto: false },
36
- { id: 'C15', cat: 'Preliminary (15%)', label: 'Stock_In PK indicated in ERD', auto: false },
37
- { id: 'C16', cat: 'Preliminary (15%)', label: 'Stock_Out PK indicated in ERD', auto: false },
38
- { id: 'C17', cat: 'Preliminary (15%)', label: 'Users FK in Stock_Out indicated', auto: false },
39
- { id: 'C18', cat: 'Preliminary (15%)', label: 'Spare_Part FK in Stock_In indicated', auto: false },
40
- { id: 'C19', cat: 'Preliminary (15%)', label: 'Spare_Part FK in Stock_Out indicated', auto: false },
41
- { id: 'C20', cat: 'Process (50%)', label: 'Project folder named correctly (FirstName_LastName_Exam)', auto: false },
42
- { id: 'C21', cat: 'Process (50%)', label: 'Node.js project created (package.json exists)', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'package.json')) },
43
- { id: 'C22', cat: 'Process (50%)', label: 'Express.js installed', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'node_modules', 'express')) },
44
- { id: 'C23', cat: 'Process (50%)', label: 'Cors installed', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'node_modules', 'cors')) },
45
- { id: 'C24', cat: 'Process (50%)', label: 'Nodemon installed', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'node_modules', 'nodemon')) },
46
- { id: 'C25', cat: 'Process (50%)', label: 'MySQL2 installed in Node.js', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'node_modules', 'mysql2')) },
47
- { id: 'C26', cat: 'Process (50%)', label: 'React project created', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'package.json')) },
48
- { id: 'C27', cat: 'Process (50%)', label: 'React-router-dom installed', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'node_modules', 'react-router-dom')) },
49
- { id: 'C28', cat: 'Process (50%)', label: 'Axios installed', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'node_modules', 'axios')) },
50
- { id: 'C29', cat: 'Process (50%)', label: 'SIMS/CRPMS database created', auto: true, check: () => { try { execSync('mysql -u root -e "SHOW DATABASES;" 2>nul', { stdio: 'pipe', encoding: 'utf8' }); return true; } catch { return false; } } },
51
- { id: 'C30', cat: 'Process (50%)', label: 'Users table created', auto: false },
52
- { id: 'C31', cat: 'Process (50%)', label: 'Spare_Part/Services table created', auto: false },
53
- { id: 'C32', cat: 'Process (50%)', label: 'Stock_Out/ServiceRecord table created', auto: false },
54
- { id: 'C33', cat: 'Process (50%)', label: 'Stock_In/Payment table created', auto: false },
55
- { id: 'C34', cat: 'Process (50%)', label: 'PKs applied in all tables', auto: true, check: () => existsSync(join(process.cwd(), 'database.sql')) },
56
- { id: 'C35', cat: 'Process (50%)', label: 'FKs applied in Stock_In/ServiceRecord', auto: true, check: () => existsSync(join(process.cwd(), 'database.sql')) },
57
- { id: 'C36', cat: 'Process (50%)', label: 'FKs applied in Stock_Out/Payment', auto: true, check: () => existsSync(join(process.cwd(), 'database.sql')) },
58
- { id: 'C37', cat: 'Process (50%)', label: 'React function component declared', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'src', 'App.jsx')) },
59
- { id: 'C38', cat: 'Process (50%)', label: 'React return method included', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'src', 'App.jsx')) },
60
- { id: 'C39', cat: 'Process (50%)', label: 'React function exported', auto: true, check: () => { const c = join(process.cwd(), 'frontend-project', 'src', 'App.jsx'); return existsSync(c) && readFileSync(c,'utf8').includes('export'); } },
61
- { id: 'C40', cat: 'Process (50%)', label: 'Component mounted to DOM', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'src', 'main.jsx')) },
62
- { id: 'C41', cat: 'Process (50%)', label: 'JSX used inside return method', auto: true, check: () => { const c = join(process.cwd(), 'frontend-project', 'src', 'App.jsx'); return existsSync(c) && readFileSync(c,'utf8').includes('return'); } },
63
- { id: 'C42', cat: 'Process (50%)', label: 'Login form created', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'src', 'pages')) },
64
- { id: 'C43', cat: 'Process (50%)', label: 'Spare_Part/Services form created', auto: false },
65
- { id: 'C44', cat: 'Process (50%)', label: 'Stock_In/ServiceRecord form created', auto: false },
66
- { id: 'C45', cat: 'Process (50%)', label: 'Stock_Out/Payment form created', auto: false },
67
- { id: 'C46', cat: 'Process (50%)', label: 'Routes configured', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'node_modules', 'react-router-dom')) },
68
- { id: 'C47', cat: 'Process (50%)', label: 'Links created between components', auto: false },
69
- { id: 'C48', cat: 'Process (50%)', label: 'Navigation layout created', auto: true, check: () => { try { const p = join(process.cwd(), 'frontend-project', 'src', 'components'); return existsSync(p) && readdirSync(p).length > 0; } catch { return false; } } },
70
- { id: 'C49', cat: 'Process (50%)', label: 'Tailwind CSS installed', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'node_modules', 'tailwindcss')) },
71
- { id: 'C50', cat: 'Process (50%)', label: 'Tailwind CSS configured', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'tailwind.config.js')) || existsSync(join(process.cwd(), 'frontend-project', 'vite.config.js')) },
72
- { id: 'C51', cat: 'Process (50%)', label: 'Server JS file created', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'server.js')) },
73
- { id: 'C52', cat: 'Process (50%)', label: 'Express imported', auto: true, check: () => { const s = join(process.cwd(), 'backend-project', 'server.js'); return existsSync(s) && readFileSync(s,'utf8').includes('express'); } },
74
- { id: 'C53', cat: 'Process (50%)', label: 'Cors imported', auto: true, check: () => { const s = join(process.cwd(), 'backend-project', 'server.js'); return existsSync(s) && readFileSync(s,'utf8').includes('cors'); } },
75
- { id: 'C54', cat: 'Process (50%)', label: 'Port number identified', auto: true, check: () => { const s = join(process.cwd(), 'backend-project', 'server.js'); return existsSync(s) && readFileSync(s,'utf8').includes('PORT'); } },
76
- { id: 'C55', cat: 'Process (50%)', label: 'Express object created', auto: true, check: () => { const s = join(process.cwd(), 'backend-project', 'server.js'); return existsSync(s) && readFileSync(s,'utf8').includes('express()'); } },
77
- { id: 'C56', cat: 'Process (50%)', label: 'Listen method applied', auto: true, check: () => { const s = join(process.cwd(), 'backend-project', 'server.js'); return existsSync(s) && readFileSync(s,'utf8').includes('listen'); } },
78
- { id: 'C57', cat: 'Process (50%)', label: 'MySQL2 package used', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'node_modules', 'mysql2')) },
79
- { id: 'C58', cat: 'Process (50%)', label: 'POST endpoint created', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'routes')) },
80
- { id: 'C59', cat: 'Process (50%)', label: 'GET endpoint created', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'routes')) },
81
- { id: 'C60', cat: 'Process (50%)', label: 'PUT endpoint created', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'routes')) },
82
- { id: 'C61', cat: 'Process (50%)', label: 'DELETE endpoint created', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'routes')) },
83
- { id: 'C62', cat: 'Process (50%)', label: 'Axios imported in frontend', auto: true, check: () => existsSync(join(process.cwd(), 'frontend-project', 'node_modules', 'axios')) },
84
- { id: 'C63', cat: 'Process (50%)', label: 'Password input encrypted (bcryptjs)', auto: true, check: () => existsSync(join(process.cwd(), 'backend-project', 'node_modules', 'bcryptjs')) },
85
- { id: 'C64', cat: 'Product (30%)', label: 'Project is presented', auto: false },
86
- { id: 'C65', cat: 'Product (30%)', label: 'Input validation prevents invalid data', auto: false },
87
- { id: 'C66', cat: 'Product (30%)', label: 'Form submission saves data', auto: false },
88
- { id: 'C67', cat: 'Product (30%)', label: 'Data retrieval displays records', auto: false },
89
- { id: 'C68', cat: 'Product (30%)', label: 'Update functionality works', auto: false },
90
- { id: 'C69', cat: 'Product (30%)', label: 'Delete functionality works', auto: false },
91
- { id: 'C70', cat: 'Product (30%)', label: 'Navigation menus interactive', auto: false },
92
- { id: 'C71', cat: 'Product (30%)', label: 'Daily StockOut report generated', auto: false },
93
- { id: 'C72', cat: 'Product (30%)', label: 'Daily Stock Status report generated', auto: false },
94
- { id: 'C73', cat: 'Closing (5%)', label: 'Project folder removed after marking', auto: false },
95
- { id: 'C74', cat: 'Closing (5%)', label: 'Database deleted after marking', auto: false },
96
- { id: 'C75', cat: 'Closing (5%)', label: 'Global dependencies removed', auto: false },
97
- ];
98
-
99
- const PROJECT_CHOICES = [
100
- {
101
- name: 'SIMS',
102
- description: 'Stock Inventory Management System (SmartPark)',
103
- dir: 'SIMS-master',
104
- db: 'SIMS',
105
- defaultUser: 'admin / admin123'
106
- },
107
- {
108
- name: 'CRPMS',
109
- description: 'Car Repair Payment Management System',
110
- dir: 'CRPMS-main',
111
- db: 'CRPMS',
112
- defaultUser: 'admin / Admin@1234'
17
+ function read(p) { try { return existsSync(p) ? readFileSync(p, 'utf8') : ''; } catch { return ''; } }
18
+ function inDir(p) { try { return existsSync(p) ? readdirSync(p) : []; } catch { return []; } }
19
+
20
+ const GUIDANCE = {
21
+ 'backend-package': 'Run: cd backend-project && npm init -y',
22
+ 'express': 'Run: cd backend-project && npm install express',
23
+ 'cors': 'Run: cd backend-project && npm install cors',
24
+ 'nodemon': 'Run: cd backend-project && npm install --save-dev nodemon',
25
+ 'mysql2': 'Run: cd backend-project && npm install mysql2',
26
+ 'bcryptjs': 'Run: cd backend-project && npm install bcryptjs',
27
+ 'frontend-package': 'Run: npx create-vite frontend-project --template react',
28
+ 'react-router-dom': 'Run: cd frontend-project && npm install react-router-dom',
29
+ 'axios': 'Run: cd frontend-project && npm install axios',
30
+ 'tailwindcss': 'Run: cd frontend-project && npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p',
31
+ 'server.js': 'Create backend-project/server.js with express app, cors, session, routes, and app.listen(PORT)',
32
+ 'db.js': 'Create backend-project/config/db.js with mysql2/promise pool configuration',
33
+ 'routes': 'Create route files in backend-project/routes/ with router.get(), router.post(), router.put(), router.delete()',
34
+ 'app.jsx': 'Create frontend-project/src/App.jsx with BrowserRouter, Routes, Route components',
35
+ 'pages': 'Create page components in frontend-project/src/pages/ for each form',
36
+ 'navbar': 'Create frontend-project/src/components/Navbar.jsx with navigation links and logout',
37
+ 'env': 'Create backend-project/.env file: DB_HOST=localhost DB_USER=root DB_PASSWORD= DB_NAME=SIMS PORT=5000',
38
+ 'database.sql': 'Create database.sql with CREATE DATABASE and CREATE TABLE statements',
39
+ 'vite.config': 'Create frontend-project/vite.config.js with React plugin and server port 5173',
40
+ 'main.jsx': 'Create frontend-project/src/main.jsx to mount App component to DOM',
41
+ 'index.css': 'Create frontend-project/src/index.css with @tailwind directives',
42
+ 'tailwind.config': 'Configure tailwind.config.js with content paths',
43
+ 'login-page': 'Create Login.jsx with form inputs for username/password and handleSubmit',
44
+ 'reports': 'Create backend routes for daily stockout and stock status reports',
45
+ };
46
+
47
+ const SIMS_PAGES = ['Login.jsx', 'SparePart.jsx', 'StockIn.jsx', 'StockOut.jsx', 'Reports.jsx'];
48
+ const CRPMS_PAGES = ['Login.jsx', 'Cars.jsx', 'Services.jsx', 'ServiceRecords.jsx', 'Payments.jsx', 'Reports.jsx', 'Bill.jsx'];
49
+
50
+ function buildChecklist(baseDir, projectType) {
51
+ const b = p => join(baseDir, p);
52
+ const fe = p => join(baseDir, 'frontend-project', p);
53
+ const be = p => join(baseDir, 'backend-project', p);
54
+ const src = p => join(baseDir, 'frontend-project', 'src', p);
55
+ const pages = projectType === 'SIMS' ? SIMS_PAGES : CRPMS_PAGES;
56
+ const dbName = projectType === 'SIMS' ? 'SIMS' : 'CRPMS';
57
+
58
+ const checks = [
59
+ { id: 'C1', cat: 'Preliminary (15%)', label: 'ERD Spare_Part entity drawn',
60
+ auto: () => 'MANUAL', guide: 'Draw Spare_Part entity on paper with entity symbol' },
61
+ { id: 'C2', cat: 'Preliminary (15%)', label: 'ERD Stock_In entity drawn',
62
+ auto: () => 'MANUAL', guide: 'Draw Stock_In entity on paper with entity symbol' },
63
+ { id: 'C3', cat: 'Preliminary (15%)', label: 'ERD Stock_Out entity drawn',
64
+ auto: () => 'MANUAL', guide: 'Draw Stock_Out entity on paper with entity symbol' },
65
+ { id: 'C4', cat: 'Preliminary (15%)', label: 'ERD entity symbol used',
66
+ auto: () => 'MANUAL', guide: 'Use rectangle/box symbol for each entity' },
67
+ { id: 'C5', cat: 'Preliminary (15%)', label: 'ERD relationship symbol used',
68
+ auto: () => 'MANUAL', guide: 'Use diamond/line symbols for relationships between entities' },
69
+ { id: 'C6', cat: 'Preliminary (15%)', label: 'ERD link symbol used',
70
+ auto: () => 'MANUAL', guide: 'Use connecting lines between entities and relationships' },
71
+ { id: 'C7', cat: 'Preliminary (15%)', label: 'ERD Primary Key rule respected',
72
+ auto: () => 'MANUAL', guide: 'Underline PK attributes in each entity' },
73
+ { id: 'C8', cat: 'Preliminary (15%)', label: 'ERD Foreign Key rule respected',
74
+ auto: () => 'MANUAL', guide: 'Mark FK attributes with dotted underline or FK notation' },
75
+ { id: 'C9', cat: 'Preliminary (15%)', label: 'ERD cardinalities indicated',
76
+ auto: () => 'MANUAL', guide: 'Show 1---* or M---N on relationship lines' },
77
+ { id: 'C10', cat: 'Preliminary (15%)', label: 'Spare_Part-Stock_In relationship drawn',
78
+ auto: () => 'MANUAL', guide: `Connect Spare_Part (1) to Stock_In (*) via SparePartID FK` },
79
+ { id: 'C11', cat: 'Preliminary (15%)', label: 'Users-Stock_Out relationship drawn',
80
+ auto: () => 'MANUAL', guide: 'Connect Users (1) to Stock_Out (*) via UserID FK' },
81
+ { id: 'C12', cat: 'Preliminary (15%)', label: 'Spare_Part-Stock_Out relationship drawn',
82
+ auto: () => 'MANUAL', guide: `Connect Spare_Part (1) to Stock_Out (*) via SparePartID FK` },
83
+ { id: 'C13', cat: 'Preliminary (15%)', label: 'Users PK indicated',
84
+ auto: () => 'MANUAL', guide: 'Underline UserID in Users entity' },
85
+ { id: 'C14', cat: 'Preliminary (15%)', label: 'Spare_Part PK indicated',
86
+ auto: () => 'MANUAL', guide: 'Underline SparePartID in Spare_Part entity' },
87
+ { id: 'C15', cat: 'Preliminary (15%)', label: 'Stock_In PK indicated',
88
+ auto: () => 'MANUAL', guide: 'Underline StockInID in Stock_In entity' },
89
+ { id: 'C16', cat: 'Preliminary (15%)', label: 'Stock_Out PK indicated',
90
+ auto: () => 'MANUAL', guide: 'Underline StockOutID in Stock_Out entity' },
91
+ { id: 'C17', cat: 'Preliminary (15%)', label: 'Users FK in Stock_Out indicated',
92
+ auto: () => 'MANUAL', guide: 'Mark UserID as FK in Stock_Out entity' },
93
+ { id: 'C18', cat: 'Preliminary (15%)', label: 'Spare_Part FK in Stock_In indicated',
94
+ auto: () => 'MANUAL', guide: 'Mark SparePartID as FK in Stock_In entity' },
95
+ { id: 'C19', cat: 'Preliminary (15%)', label: 'Spare_Part FK in Stock_Out indicated',
96
+ auto: () => 'MANUAL', guide: 'Mark SparePartID as FK in Stock_Out entity' },
97
+
98
+ { id: 'C20', cat: 'Process (50%)', label: 'Project folder named FirstName_LastName_Exam_2025',
99
+ auto: () => !baseDir.includes('node_modules') && baseDir.split('_').length >= 3 ? 'PASS' : 'FAIL',
100
+ guide: 'Rename parent folder to FirstName_LastName_National_Practical_Exam_2025' },
101
+ { id: 'C21', cat: 'Process (50%)', label: 'Node.js project created (package.json)',
102
+ auto: () => existsSync(be('package.json')) ? 'PASS' : 'FAIL',
103
+ guide: GUIDANCE['backend-package'] },
104
+ { id: 'C22', cat: 'Process (50%)', label: 'Express.js installed',
105
+ auto: () => existsSync(be('node_modules/express')) ? 'PASS' : 'FAIL',
106
+ guide: GUIDANCE['express'] },
107
+ { id: 'C23', cat: 'Process (50%)', label: 'Cors installed',
108
+ auto: () => existsSync(be('node_modules/cors')) ? 'PASS' : 'FAIL',
109
+ guide: GUIDANCE['cors'] },
110
+ { id: 'C24', cat: 'Process (50%)', label: 'Nodemon installed',
111
+ auto: () => existsSync(be('node_modules/nodemon')) ? 'PASS' : 'FAIL',
112
+ guide: GUIDANCE['nodemon'] },
113
+ { id: 'C25', cat: 'Process (50%)', label: 'MySQL2 installed in Node.js',
114
+ auto: () => existsSync(be('node_modules/mysql2')) ? 'PASS' : 'FAIL',
115
+ guide: GUIDANCE['mysql2'] },
116
+ { id: 'C26', cat: 'Process (50%)', label: 'React project created (vite)',
117
+ auto: () => existsSync(fe('package.json')) && existsSync(fe('vite.config.js')) ? 'PASS' : 'FAIL',
118
+ guide: GUIDANCE['frontend-package'] },
119
+ { id: 'C27', cat: 'Process (50%)', label: 'React-router-dom installed',
120
+ auto: () => existsSync(fe('node_modules/react-router-dom')) ? 'PASS' : 'FAIL',
121
+ guide: GUIDANCE['react-router-dom'] },
122
+ { id: 'C28', cat: 'Process (50%)', label: 'Axios installed',
123
+ auto: () => existsSync(fe('node_modules/axios')) ? 'PASS' : 'FAIL',
124
+ guide: GUIDANCE['axios'] },
125
+ { id: 'C29', cat: 'Process (50%)', label: `${dbName} database SQL created`,
126
+ auto: () => existsSync(b('database.sql')) && read(b('database.sql')).includes(dbName) ? 'PASS' : 'FAIL',
127
+ guide: `Create database.sql with CREATE DATABASE ${dbName}` },
128
+ { id: 'C30', cat: 'Process (50%)', label: 'Users table created in SQL',
129
+ auto: () => read(b('database.sql')).includes('CREATE TABLE') && read(b('database.sql')).toLowerCase().includes('user') ? 'PASS' : 'FAIL',
130
+ guide: 'Add CREATE TABLE Users (UserID INT PK, Username, Password)' },
131
+ { id: 'C31', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Spare_Part' : 'Services'} table created`,
132
+ auto: () => { const s = read(b('database.sql')).toLowerCase(); return projectType === 'SIMS' ? s.includes('spare_part') : s.includes('services'); } ? 'PASS' : 'FAIL',
133
+ guide: projectType === 'SIMS' ? 'Add CREATE TABLE Spare_Part (SparePartID PK, Name, Category, Quantity, UnitPrice, TotalPrice)' : 'Add CREATE TABLE Services (ServiceCode PK, ServiceName, ServicePrice)' },
134
+ { id: 'C32', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_Out' : 'ServiceRecord'} table created`,
135
+ auto: () => { const s = read(b('database.sql')).toLowerCase(); return projectType === 'SIMS' ? s.includes('stock_out') : s.includes('servicerecord'); } ? 'PASS' : 'FAIL',
136
+ guide: projectType === 'SIMS' ? 'Add CREATE TABLE Stock_Out (StockOutID PK, SparePartID FK, StockOutQuantity, StockOutUnitPrice, StockOutTotalPrice, StockOutDate)' : 'Add CREATE TABLE ServiceRecord (RecordNumber PK, ServiceDate, PlateNumber FK, ServiceCode FK)' },
137
+ { id: 'C33', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_In' : 'Payment'} table created`,
138
+ auto: () => { const s = read(b('database.sql')).toLowerCase(); return projectType === 'SIMS' ? s.includes('stock_in') : s.includes('payment'); } ? 'PASS' : 'FAIL',
139
+ guide: projectType === 'SIMS' ? 'Add CREATE TABLE Stock_In (StockInID PK, SparePartID FK, StockInQuantity, StockInDate)' : 'Add CREATE TABLE Payment (PaymentNumber PK, AmountPaid, PaymentDate, RecordNumber FK, UserID FK)' },
140
+ { id: 'C34', cat: 'Process (50%)', label: 'Primary keys applied in all tables',
141
+ auto: () => read(b('database.sql')).includes('PRIMARY KEY') ? 'PASS' : 'FAIL',
142
+ guide: 'Add PRIMARY KEY to each table definition' },
143
+ { id: 'C35', cat: 'Process (50%)', label: `FKs applied in ${projectType === 'SIMS' ? 'Stock_In' : 'ServiceRecord'}`,
144
+ auto: () => read(b('database.sql')).includes('FOREIGN KEY') ? 'PASS' : 'FAIL',
145
+ guide: `Add FOREIGN KEY references in ${projectType === 'SIMS' ? 'Stock_In (SparePartID -> Spare_Part)' : 'ServiceRecord (PlateNumber -> Car, ServiceCode -> Services)'}` },
146
+ { id: 'C36', cat: 'Process (50%)', label: `FKs applied in ${projectType === 'SIMS' ? 'Stock_Out' : 'Payment'}`,
147
+ auto: () => { const s = read(b('database.sql')); return s.includes('FOREIGN KEY'); } ? 'PASS' : 'FAIL',
148
+ guide: `Add FOREIGN KEY in ${projectType === 'SIMS' ? 'Stock_Out' : 'Payment'}` },
149
+ { id: 'C37', cat: 'Process (50%)', label: 'React function component declared',
150
+ auto: () => read(src('App.jsx')).includes('function') || read(src('App.jsx')).includes('=>') || read(src('App.jsx')).includes('export default function') ? 'PASS' : 'FAIL',
151
+ guide: 'Use function App() { ... } in App.jsx' },
152
+ { id: 'C38', cat: 'Process (50%)', label: 'Return method included in component',
153
+ auto: () => read(src('App.jsx')).includes('return') ? 'PASS' : 'FAIL',
154
+ guide: 'Add return ( <JSX> ) in your component' },
155
+ { id: 'C39', cat: 'Process (50%)', label: 'Component exported',
156
+ auto: () => read(src('App.jsx')).includes('export default') ? 'PASS' : 'FAIL',
157
+ guide: 'Add "export default App;" at the end' },
158
+ { id: 'C40', cat: 'Process (50%)', label: 'Component mounted to DOM',
159
+ auto: () => existsSync(src('main.jsx')) && read(src('main.jsx')).includes('ReactDOM') ? 'PASS' : 'FAIL',
160
+ guide: 'In main.jsx: createRoot(document.getElementById("root")).render(<App />)' },
161
+ { id: 'C41', cat: 'Process (50%)', label: 'JSX used inside return method',
162
+ auto: () => read(src('App.jsx')).includes('return') && (read(src('App.jsx')).includes('<') || read(src('App.jsx')).includes('React.createElement')) ? 'PASS' : 'FAIL',
163
+ guide: 'Add JSX markup inside return()' },
164
+ { id: 'C42', cat: 'Process (50%)', label: 'Login form created',
165
+ auto: () => existsSync(src('pages/Login.jsx')) ? 'PASS' : 'FAIL',
166
+ guide: 'Create src/pages/Login.jsx with username/password inputs' },
167
+ { id: 'C43', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Spare_Part' : 'Cars/Services'} form created`,
168
+ auto: () => { const p = inDir(src('pages')); return projectType === 'SIMS' ? p.some(f => f.toLowerCase().includes('spare')) : (p.some(f => f.toLowerCase().includes('car')) && p.some(f => f.toLowerCase().includes('service'))); } ? 'PASS' : 'FAIL',
169
+ guide: projectType === 'SIMS' ? 'Create SparePart.jsx with fields: Name, Category, Quantity, UnitPrice' : 'Create Cars.jsx and Services.jsx' },
170
+ { id: 'C44', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_In' : 'ServiceRecord'} form created`,
171
+ auto: () => inDir(src('pages')).some(f => f.toLowerCase().includes(projectType === 'SIMS' ? 'stockin' : 'record')) ? 'PASS' : 'FAIL',
172
+ guide: projectType === 'SIMS' ? 'Create StockIn.jsx with SparePart selection, quantity, date' : 'Create ServiceRecords.jsx' },
173
+ { id: 'C45', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_Out' : 'Payment'} form created`,
174
+ auto: () => inDir(src('pages')).some(f => f.toLowerCase().includes(projectType === 'SIMS' ? 'stockout' : 'payment')) ? 'PASS' : 'FAIL',
175
+ guide: projectType === 'SIMS' ? 'Create StockOut.jsx' : 'Create Payments.jsx' },
176
+ { id: 'C46', cat: 'Process (50%)', label: 'Routes configured in React',
177
+ auto: () => read(src('App.jsx')).includes('Routes') && read(src('App.jsx')).includes('Route') ? 'PASS' : 'FAIL',
178
+ guide: 'Add <Routes><Route path="..." element={...}></Routes> in App.jsx' },
179
+ { id: 'C47', cat: 'Process (50%)', label: 'Links created between components',
180
+ auto: () => read(src('App.jsx')).includes('Link') || read(src('components/Navbar.jsx')).includes('Link') ? 'PASS' : 'FAIL',
181
+ guide: 'Use <Link to="/path"> in Navbar or components for navigation' },
182
+ { id: 'C48', cat: 'Process (50%)', label: 'Navigation layout created',
183
+ auto: () => read(src('components/Navbar.jsx')).length > 50 ? 'PASS' : 'FAIL',
184
+ guide: 'Create Navbar.jsx with links to all pages and logout button' },
185
+ { id: 'C49', cat: 'Process (50%)', label: 'Tailwind CSS installed',
186
+ auto: () => existsSync(fe('node_modules/tailwindcss')) ? 'PASS' : 'FAIL',
187
+ guide: GUIDANCE['tailwindcss'] },
188
+ { id: 'C50', cat: 'Process (50%)', label: 'Tailwind CSS configured',
189
+ auto: () => (existsSync(fe('tailwind.config.js')) && read(fe('tailwind.config.js')).includes('content')) || read(fe('vite.config.js')).includes('tailwindcss') || read(fe('postcss.config.js')).includes('tailwind') ? 'PASS' : 'FAIL',
190
+ guide: 'Configure tailwind.config.js with content paths or use @tailwindcss/vite plugin' },
191
+ { id: 'C51', cat: 'Process (50%)', label: 'Server JS file created',
192
+ auto: () => existsSync(be('server.js')) ? 'PASS' : 'FAIL',
193
+ guide: GUIDANCE['server.js'] },
194
+ { id: 'C52', cat: 'Process (50%)', label: 'Express package imported',
195
+ auto: () => read(be('server.js')).includes('express') ? 'PASS' : 'FAIL',
196
+ guide: 'Add "const express = require(\'express\')" at top of server.js' },
197
+ { id: 'C53', cat: 'Process (50%)', label: 'Cors package imported',
198
+ auto: () => read(be('server.js')).includes('cors') ? 'PASS' : 'FAIL',
199
+ guide: 'Add "const cors = require(\'cors\')" and app.use(cors(...)) in server.js' },
200
+ { id: 'C54', cat: 'Process (50%)', label: 'Port number identified',
201
+ auto: () => read(be('server.js')).includes('PORT') ? 'PASS' : 'FAIL',
202
+ guide: 'Define "const PORT = 5000" and app.listen(PORT, ...) in server.js' },
203
+ { id: 'C55', cat: 'Process (50%)', label: 'Express object created',
204
+ auto: () => read(be('server.js')).includes('express()') ? 'PASS' : 'FAIL',
205
+ guide: 'Add "const app = express()" in server.js' },
206
+ { id: 'C56', cat: 'Process (50%)', label: 'Listen method applied',
207
+ auto: () => read(be('server.js')).includes('listen') ? 'PASS' : 'FAIL',
208
+ guide: 'Add "app.listen(PORT, () => console.log(...))" in server.js' },
209
+ { id: 'C57', cat: 'Process (50%)', label: 'MySQL2 package used in backend',
210
+ auto: () => existsSync(be('node_modules/mysql2')) ? 'PASS' : 'FAIL',
211
+ guide: GUIDANCE['mysql2'] },
212
+ { id: 'C58', cat: 'Process (50%)', label: 'POST endpoint created',
213
+ auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.post')) ? 'PASS' : 'FAIL',
214
+ guide: 'Add router.post(\'/\', async (req, res) => {...}) in route files' },
215
+ { id: 'C59', cat: 'Process (50%)', label: 'GET endpoint created',
216
+ auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.get')) ? 'PASS' : 'FAIL',
217
+ guide: 'Add router.get(\'/\', async (req, res) => {...}) in route files' },
218
+ { id: 'C60', cat: 'Process (50%)', label: 'PUT endpoint created',
219
+ auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.put')) ? 'PASS' : 'FAIL',
220
+ guide: 'Add router.put(\'/:id\', async (req, res) => {...}) in route files' },
221
+ { id: 'C61', cat: 'Process (50%)', label: 'DELETE endpoint created',
222
+ auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.delete')) ? 'PASS' : 'FAIL',
223
+ guide: 'Add router.delete(\'/:id\', async (req, res) => {...}) in route files' },
224
+ { id: 'C62', cat: 'Process (50%)', label: 'Axios imported/used in frontend',
225
+ auto: () => read(src('App.jsx')).includes('axios') || existsSync(src('api/axios.js')) ? 'PASS' : 'FAIL',
226
+ guide: 'Add "import axios from \'axios\'" and set axios.defaults.baseURL' },
227
+ { id: 'C63', cat: 'Process (50%)', label: 'Password encrypted (bcryptjs)',
228
+ auto: () => existsSync(be('node_modules/bcryptjs')) ? 'PASS' : 'FAIL',
229
+ guide: GUIDANCE['bcryptjs'] },
230
+ { id: 'C64', cat: 'Process (50%)', label: 'Express-session installed',
231
+ auto: () => existsSync(be('node_modules/express-session')) ? 'PASS' : 'FAIL',
232
+ guide: 'Run: cd backend-project && npm install express-session' },
233
+ { id: 'C65', cat: 'Process (50%)', label: 'Session-based login implemented',
234
+ auto: () => read(be('server.js')).includes('session') && read(be('routes/auth.js')).includes('session') ? 'PASS' : 'FAIL',
235
+ guide: 'Use express-session with req.session.user to manage login state' },
236
+ { id: 'C66', cat: 'Process (50%)', label: 'DB config file exists',
237
+ auto: () => existsSync(be('config/db.js')) ? 'PASS' : 'FAIL',
238
+ guide: 'Create config/db.js with mysql2/promise pool using env variables' },
239
+ { id: 'C67', cat: 'Process (50%)', label: '.env file configured',
240
+ auto: () => existsSync(be('.env')) || existsSync(be('.env.example')) ? 'PASS' : 'FAIL',
241
+ guide: GUIDANCE['env'] },
242
+ { id: 'C68', cat: 'Process (50%)', label: 'Tailwind CSS imported in index.css',
243
+ auto: () => read(src('index.css')).includes('tailwind') ? 'PASS' : 'FAIL',
244
+ guide: 'Add @tailwind base; @tailwind components; @tailwind utilities; in index.css' },
245
+ { id: 'C69', cat: 'Process (50%)', label: 'Vite config for React',
246
+ auto: () => read(fe('vite.config.js')).includes('@vitejs/plugin-react') ? 'PASS' : 'FAIL',
247
+ guide: 'Add @vitejs/plugin-react in vite.config.js plugins array' },
248
+ { id: 'C70', cat: 'Product (30%)', label: 'Project is presentable',
249
+ auto: () => 'MANUAL', guide: 'Prepare a brief presentation covering: product name, key steps, importance, challenges, solutions' },
250
+ { id: 'C71', cat: 'Product (30%)', label: 'Input validation prevents invalid data',
251
+ auto: () => { const r = read(be('routes/stockOut.js')) || read(be('routes/payments.js')); return r.includes('return res.status(400)') ? 'PASS' : 'FAIL';
252
+ }, guide: 'Add validation checks: if (!field) return res.status(400).json({error: "Field required"})' },
253
+ { id: 'C72', cat: 'Product (30%)', label: 'Form submission saves data to DB',
254
+ auto: () => { const r = inDir(be('routes')); return r.some(f => read(be('routes/'+f)).includes('INSERT INTO') || read(be('routes/'+f)).includes('res.status(201)')); } ? 'PASS' : 'FAIL',
255
+ guide: 'Use INSERT INTO SQL statements in POST route handlers' },
256
+ { id: 'C73', cat: 'Product (30%)', label: 'Data retrieval displays records',
257
+ auto: () => { const r = inDir(be('routes')); return r.some(f => read(be('routes/'+f)).includes('SELECT') && read(be('routes/'+f)).includes('router.get')); } ? 'PASS' : 'FAIL',
258
+ guide: 'Use SELECT queries in GET route handlers and return JSON' },
259
+ { id: 'C74', cat: 'Product (30%)', label: 'Update functionality works (PUT)',
260
+ auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('UPDATE') && read(be('routes/'+f)).includes('router.put')) ? 'PASS' : 'FAIL',
261
+ guide: 'Use UPDATE SQL in PUT route handlers' },
262
+ { id: 'C75', cat: 'Product (30%)', label: 'Delete functionality works (DELETE)',
263
+ auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('DELETE') && read(be('routes/'+f)).includes('router.delete')) ? 'PASS' : 'FAIL',
264
+ guide: 'Use DELETE SQL in DELETE route handlers' },
265
+ { id: 'C76', cat: 'Product (30%)', label: 'Navigation menus are interactive',
266
+ auto: () => read(src('components/Navbar.jsx')).includes('useState') && read(src('components/Navbar.jsx')).includes('onClick') ? 'PASS' : 'FAIL',
267
+ guide: 'Add onClick handlers and responsive toggle (hamburger menu) in Navbar' },
268
+ { id: 'C77', cat: 'Product (30%)', label: 'Buttons are interactive',
269
+ auto: () => (read(src('pages/Login.jsx')).includes('onClick') || read(src('pages/Login.jsx')).includes('onSubmit')) ? 'PASS' : 'FAIL',
270
+ guide: 'Add onClick or onSubmit event handlers to buttons' },
271
+ { id: 'C78', cat: 'Product (30%)', label: 'Responsive design (flexbox/grid)',
272
+ auto: () => (read(src('components/Navbar.jsx')).includes('md:') || read(src('App.jsx')).includes('flex') || read(src('index.css')).includes('grid')) ? 'PASS' : 'FAIL',
273
+ guide: 'Use Tailwind responsive prefixes (sm:, md:, lg:) and flexbox/grid' },
274
+ { id: 'C79', cat: 'Product (30%)', label: `Daily ${projectType === 'SIMS' ? 'StockOut' : 'Service'} report generated`,
275
+ auto: () => read(be('routes/reports.js')).includes(projectType === 'SIMS' ? 'daily-stockout' : '/daily') ? 'PASS' : 'FAIL',
276
+ guide: `Add GET /api/reports/${projectType === 'SIMS' ? 'daily-stockout' : 'daily'} endpoint in routes/reports.js` },
277
+ { id: 'C80', cat: 'Product (30%)', label: `Daily Stock Status report generated`,
278
+ auto: () => read(be('routes/reports.js')).includes('stock-status') || read(be('routes/reports.js')).includes('summary') ? 'PASS' : 'FAIL',
279
+ guide: `Add GET /api/reports/stock-status with Stored Quantity, StockOut, Remaining query` },
280
+ { id: 'C81', cat: 'Product (30%)', label: 'Colors and styling are consistent',
281
+ auto: () => read(src('index.css')).length > 0 && read(fe('tailwind.config.js') || '').length > 0 ? 'PASS' : 'FAIL',
282
+ guide: 'Use a consistent color palette in Tailwind config (primary, secondary, accent)' },
283
+ { id: 'C82', cat: 'Closing (5%)', label: 'Project folder can be removed after marking',
284
+ auto: () => 'MANUAL', guide: 'After assessor approval, delete the project folder' },
285
+ { id: 'C83', cat: 'Closing (5%)', label: 'Database can be deleted after marking',
286
+ auto: () => 'MANUAL', guide: 'Run: DROP DATABASE ' + dbName + '; in MySQL' },
287
+ { id: 'C84', cat: 'Closing (5%)', label: 'Global dependencies removed',
288
+ auto: () => 'MANUAL', guide: 'Uninstall global packages: npm uninstall -g nodemon etc.' },
289
+ ];
290
+ return checks;
291
+ }
292
+
293
+ function showResults(results) {
294
+ const groups = {};
295
+ for (const r of results) {
296
+ if (!groups[r.cat]) groups[r.cat] = [];
297
+ groups[r.cat].push(r);
113
298
  }
114
- ];
115
-
116
- async function copyProjectFiles(srcDir, destDir) {
117
- const entries = readdirSync(srcDir);
118
- for (const entry of entries) {
119
- if (entry === 'node_modules') continue;
120
- const srcPath = join(srcDir, entry);
121
- const destPath = join(destDir, entry);
122
- const stat = statSync(srcPath);
123
- if (stat.isDirectory()) {
124
- fs.copySync(srcPath, destPath, { filter: (f) => !f.includes('node_modules') });
125
- } else {
126
- copyFileSync(srcPath, destPath);
299
+
300
+ log(chalk.bold('\n═══════════════════════════════════════════════════════════════'));
301
+ log(chalk.bold(' AUTO-ASSESSMENT RESULTS'));
302
+ log(chalk.bold('═══════════════════════════════════════════════════════════════\n'));
303
+
304
+ const total = results.length;
305
+ let passed = 0, failed = 0, manual = 0;
306
+
307
+ for (const [cat, items] of Object.entries(groups)) {
308
+ log(chalk.bold(chalk.bgHex('#1e293b').white(` ${cat} `)));
309
+ log('');
310
+ for (const r of items) {
311
+ if (r.status === 'PASS') { passed++;
312
+ log(` ${chalk.green('✓')} ${r.label.padEnd(58)} ${chalk.green('PASS')}`); }
313
+ else if (r.status === 'FAIL') { failed++;
314
+ log(` ${chalk.red('✗')} ${r.label.padEnd(58)} ${chalk.red('FAIL')}`); }
315
+ else { manual++;
316
+ log(` ${chalk.cyan('?')} ${r.label.padEnd(58)} ${chalk.cyan('MANUAL')}`); }
127
317
  }
318
+ log('');
128
319
  }
129
- }
130
320
 
131
- async function runInstall(dir, label) {
132
- log(chalk.cyan(`\n Installing ${label} dependencies...`));
133
- process.chdir(dir);
134
- try {
135
- const { stdout, stderr } = await execAsync('npm install', { timeout: 120000, windowsHide: true });
136
- if (stdout) log(stdout);
137
- if (stderr) log(chalk.yellow(stderr));
138
- log(chalk.green(` ${label} dependencies installed`));
139
- } catch (err) {
140
- log(chalk.red(` ✗ npm install failed for ${label}: ${err.message}`));
141
- throw err;
321
+ log(chalk.bold('───────────────────────────────────────────────────────────────'));
322
+ log(chalk.bold(` ${chalk.green('PASSED')}: ${passed} | ${chalk.red('FAILED')}: ${failed} | ${chalk.cyan('MANUAL')}: ${manual} | TOTAL: ${total}`));
323
+ const autoTotal = total - manual;
324
+ const autoPassed = passed;
325
+ const pct = autoTotal > 0 ? ((autoPassed / autoTotal) * 100).toFixed(1) : 'N/A';
326
+ log(chalk.bold(` Auto-check score: ${pct}% (${autoPassed}/${autoTotal} auto-checkable items)`));
327
+
328
+ if (failed > 0) {
329
+ log(chalk.bold(chalk.yellow('\n─── HOW TO FIX FAILED ITEMS ───\n')));
330
+ for (const r of results) {
331
+ if (r.status === 'FAIL' && r.guide) {
332
+ log(` ${chalk.red('✗')} ${chalk.bold(r.label)}`);
333
+ log(` ${chalk.dim('→')} ${r.guide}\n`);
334
+ }
335
+ }
142
336
  }
337
+
338
+ log(chalk.bold('═══════════════════════════════════════════════════════════════\n'));
143
339
  }
144
340
 
145
- async function runChecklist(projectType) {
146
- log(chalk.bold.hex('#FFA500')('\n══════════════════════════════════════════════'));
147
- log(chalk.bold.hex('#FFA500')(' NESA NATIONAL PRACTICAL EXAM CHECKLIST '));
148
- log(chalk.bold.hex('#FFA500')('══════════════════════════════════════════════\n'));
341
+ function generateMermaidDiagrams(projectType) {
342
+ let erd = '', dfd = '';
343
+ if (projectType === 'SIMS') {
344
+ erd = `erDiagram
345
+ Users ||--o{ Stock_Out : "records"
346
+ Spare_Part ||--o{ Stock_In : "receives"
347
+ Spare_Part ||--o{ Stock_Out : "issues"
348
+
349
+ Users {
350
+ int UserID PK "Auto Increment"
351
+ varchar Username "Unique"
352
+ varchar Password "Hashed"
353
+ }
354
+
355
+ Spare_Part {
356
+ int SparePartID PK "Auto Increment"
357
+ varchar Name
358
+ varchar Category
359
+ int Quantity
360
+ decimal UnitPrice
361
+ decimal TotalPrice "Generated: Qty * UnitPrice"
362
+ }
363
+
364
+ Stock_In {
365
+ int StockInID PK "Auto Increment"
366
+ int SparePartID FK
367
+ int StockInQuantity
368
+ date StockInDate
369
+ }
149
370
 
150
- log(chalk.bold(` Project: ${projectType}`));
151
- log(chalk.dim(` ${new Date().toLocaleDateString()}\n`));
371
+ Stock_Out {
372
+ int StockOutID PK "Auto Increment"
373
+ int SparePartID FK
374
+ int UserID FK
375
+ int StockOutQuantity
376
+ decimal StockOutUnitPrice
377
+ decimal StockOutTotalPrice
378
+ date StockOutDate
379
+ }`;
380
+
381
+ dfd = `graph TD
382
+ subgraph "Context Level 0"
383
+ SM[Stock Manager] -->|Input Data| SIMS[SIMS System]
384
+ SIMS -->|Reports| SM
385
+ end
386
+
387
+ subgraph "Level 1 - Processes"
388
+ P1["1.0 Manage Spare Parts"]
389
+ P2["2.0 Record Stock In"]
390
+ P3["3.0 Record Stock Out"]
391
+ P4["4.0 Generate Reports"]
392
+ P5["5.0 Authentication"]
393
+ end
394
+
395
+ subgraph "Data Stores"
396
+ D1[(Spare_Part)]
397
+ D2[(Stock_In)]
398
+ D3[(Stock_Out)]
399
+ D4[(Users)]
400
+ end
401
+
402
+ SM -->|"Login Credentials"| P5
403
+ P5 --> D4
404
+ SM -->|"Spare Part Details"| P1
405
+ P1 --> D1
406
+ SM -->|"Stock In Data"| P2
407
+ P2 --> D2
408
+ P2 -->|"Update Stock"| D1
409
+ SM -->|"Stock Out Data"| P3
410
+ P3 --> D3
411
+ P3 -->|"Reduce Stock"| D1
412
+ P3 -->|"Record User"| D4
413
+ SM -->|"Request Report"| P4
414
+ P4 --> D1
415
+ P4 --> D2
416
+ P4 --> D3`;
417
+ } else {
418
+ erd = `erDiagram
419
+ Car ||--o{ ServiceRecord : "has"
420
+ Services ||--o{ ServiceRecord : "includes"
421
+ ServiceRecord ||--o{ Payment : "generates"
422
+ User ||--o{ Payment : "processes"
423
+
424
+ Car {
425
+ varchar PlateNumber PK
426
+ varchar type
427
+ varchar Model
428
+ int ManufacturingYear
429
+ varchar DriverPhone
430
+ varchar MechanicName
431
+ }
152
432
 
153
- const results = [];
154
- let currentCat = '';
433
+ Services {
434
+ varchar ServiceCode PK
435
+ varchar ServiceName
436
+ decimal ServicePrice
437
+ }
155
438
 
156
- for (const item of CHECKLIST) {
157
- if (item.cat !== currentCat) {
158
- currentCat = item.cat;
159
- log(chalk.bold(chalk.bgBlue(`\n ${item.cat}`)));
439
+ ServiceRecord {
440
+ int RecordNumber PK "Auto Increment"
441
+ date ServiceDate
442
+ varchar PlateNumber FK
443
+ varchar ServiceCode FK
160
444
  }
161
445
 
162
- if (item.auto && item.check) {
163
- const passed = item.check();
164
- const mark = passed ? chalk.green('✓') : chalk.red('✗');
165
- const status = passed ? chalk.green('PASS') : chalk.red('FAIL');
166
- log(` ${mark} ${item.label.padEnd(55)} ${status}`);
167
- results.push({ ...item, passed, reason: passed ? '' : 'Auto-check failed' });
168
- } else {
169
- const { confirmed } = await inquirer.prompt([{
170
- type: 'confirm',
171
- name: 'confirmed',
172
- message: `${item.label}`,
173
- default: true
174
- }]);
175
- const mark = confirmed ? chalk.green('✓') : chalk.red('✗');
176
- const status = confirmed ? chalk.green('PASS') : chalk.red('FAIL');
177
- log(` ${mark} ${item.label.padEnd(55)} ${status}`);
178
- results.push({ ...item, passed: confirmed, reason: '' });
446
+ Payment {
447
+ int PaymentNumber PK "Auto Increment"
448
+ decimal AmountPaid
449
+ date PaymentDate
450
+ int RecordNumber FK
451
+ int UserID FK
179
452
  }
453
+
454
+ User {
455
+ int UserID PK "Auto Increment"
456
+ varchar Username "Unique"
457
+ varchar Password "Hashed"
458
+ varchar FullName
459
+ }`;
460
+
461
+ dfd = `graph TD
462
+ subgraph "Context Level 0"
463
+ CM[Cashier/Mechanic] -->|Input Data| CRPMS[CRPMS System]
464
+ CRPMS -->|Reports & Bills| CM
465
+ end
466
+
467
+ subgraph "Level 1 - Processes"
468
+ P1["1.0 Manage Cars"]
469
+ P2["2.0 Manage Services"]
470
+ P3["3.0 Record Service Records"]
471
+ P4["4.0 Record Payments"]
472
+ P5["5.0 Generate Reports & Bills"]
473
+ P6["6.0 Authentication"]
474
+ end
475
+
476
+ subgraph "Data Stores"
477
+ D1[(Car)]
478
+ D2[(Services)]
479
+ D3[(ServiceRecord)]
480
+ D4[(Payment)]
481
+ D5[(User)]
482
+ end
483
+
484
+ CM -->|"Login"| P6
485
+ P6 --> D5
486
+ CM -->|"Car Details"| P1
487
+ P1 --> D1
488
+ CM -->|"Service Types"| P2
489
+ P2 --> D2
490
+ CM -->|"Service Record"| P3
491
+ P3 --> D1
492
+ P3 --> D2
493
+ P3 --> D3
494
+ CM -->|"Payment Data"| P4
495
+ P4 --> D3
496
+ P4 --> D4
497
+ P4 --> D5
498
+ CM -->|"Request Report/Bill"| P5
499
+ P5 --> D1
500
+ P5 --> D2
501
+ P5 --> D3
502
+ P5 --> D4`;
180
503
  }
504
+ return { erd, dfd };
505
+ }
181
506
 
182
- const total = results.length;
183
- const passed = results.filter(r => r.passed).length;
184
- const pct = ((passed / total) * 100).toFixed(1);
185
-
186
- log(chalk.bold('\n══════════════════════════════════════════════'));
187
- log(chalk.bold(` RESULTS: ${chalk.green(passed)}/${total} passed (${pct}%)`));
188
- log(chalk.bold('══════════════════════════════════════════════\n'));
189
-
190
- const reportDir = join(process.cwd(), 'checklist_report');
191
- mkdirSync(reportDir, { recursive: true });
192
- const reportFile = join(reportDir, `checklist_${projectType}_${Date.now()}.json`);
193
- writeFileSync(reportFile, JSON.stringify({ project: projectType, date: new Date().toISOString(), total, passed, percentage: pct, results }, null, 2));
194
- log(chalk.dim(` Report saved: ${reportFile}\n`));
507
+ function initMermaidFile(filePath, projectType) {
508
+ const diags = generateMermaidDiagrams(projectType);
509
+ const title = projectType === 'SIMS' ? 'Stock Inventory Management System (SIMS)' : 'Car Repair Payment Management System (CRPMS)';
510
+ const content = `# ${title} - ERD & DFD Diagrams
511
+ ## Project: ${projectType}
512
+
513
+ ---
514
+
515
+ ## Entity Relationship Diagram (ERD)
516
+
517
+ \`\`\`mermaid
518
+ ${diags.erd}
519
+ \`\`\`
520
+
521
+ ---
522
+
523
+ ## Data Flow Diagram (DFD)
524
+
525
+ \`\`\`mermaid
526
+ ${diags.dfd}
527
+ \`\`\`
528
+
529
+ ---
530
+
531
+ *Generated by npms-exam-kit on ${new Date().toISOString().split('T')[0]}*
532
+ `;
533
+ writeFileSync(filePath, content, 'utf8');
534
+ return filePath;
195
535
  }
196
536
 
537
+ function log(m) { console.log(m); }
538
+
197
539
  async function main() {
198
540
  console.clear();
199
- log(chalk.bold.hex('#00D2FF')('\n ╔══════════════════════════════════════════════╗'));
200
- log(chalk.bold.hex('#00D2FF')(' ║ NESA NATIONAL PRACTICAL EXAM 2024-2025 ║'));
201
- log(chalk.bold.hex('#00D2FF')(' ║ SECTOR: ICT & Multimedia ║'));
202
- log(chalk.bold.hex('#00D2FF')(' ║ TRADE: Software Development ║'));
203
- log(chalk.bold.hex('#00D2FF')(' ╚══════════════════════════════════════════════╝\n'));
541
+ log(chalk.bold.hex('#00D2FF')('\n ╔══════════════════════════════════════════════════╗'));
542
+ log(chalk.bold.hex('#00D2FF')(' ║ NESA NATIONAL PRACTICAL EXAM 2024-2025 ║'));
543
+ log(chalk.bold.hex('#00D2FF')(' ║ SECTOR: ICT & Multimedia ║'));
544
+ log(chalk.bold.hex('#00D2FF')(' ║ TRADE: Software Development ║'));
545
+ log(chalk.bold.hex('#00D2FF')(' ╚══════════════════════════════════════════════════╝\n'));
546
+
547
+ const { mode } = await inquirer.prompt([{
548
+ type: 'list',
549
+ name: 'mode',
550
+ message: 'What do you want to do?',
551
+ choices: [
552
+ { name: 'Install a project + Auto-Assess', value: 'install' },
553
+ { name: 'Auto-Assess an existing project (already installed)', value: 'assess' },
554
+ { name: 'Generate ERD & DFD Mermaid diagrams for a project', value: 'mermaid' },
555
+ ]
556
+ }]);
204
557
 
205
- log(chalk.dim(' This tool will install and verify the NESA exam project(s).\n'));
558
+ const { project } = await inquirer.prompt([{
559
+ type: 'list',
560
+ name: 'project',
561
+ message: 'Select project:',
562
+ choices: [
563
+ { name: 'SIMS - Stock Inventory Management System (SmartPark)', value: 'SIMS' },
564
+ { name: 'CRPMS - Car Repair Payment Management System', value: 'CRPMS' },
565
+ ]
566
+ }]);
206
567
 
207
568
  const targetDir = join(process.cwd());
208
569
 
570
+ if (mode === 'mermaid') {
571
+ const filePath = join(targetDir, `${project}_ERD_DFD.md`);
572
+ initMermaidFile(filePath, project);
573
+ log(chalk.green(`\n ✓ Mermaid diagrams saved to: ${chalk.bold(filePath)}\n`));
574
+ log(chalk.dim(' Open this file in any Markdown viewer that supports Mermaid'));
575
+ log(chalk.dim(' (VS Code, GitHub, or https://mermaid.live)\n'));
576
+ return;
577
+ }
578
+
579
+ if (mode === 'assess') {
580
+ log(chalk.bold(`\n Running auto-assessment for ${project} in: ${targetDir}\n`));
581
+ const checks = buildChecklist(targetDir, project);
582
+ const results = checks.map(c => {
583
+ let status;
584
+ try {
585
+ const result = c.auto();
586
+ status = result === true || result === 'PASS' ? 'PASS' : (result === 'MANUAL' ? 'MANUAL' : 'FAIL');
587
+ } catch { status = 'MANUAL'; }
588
+ return { ...c, status, guide: c.guide || '' };
589
+ });
590
+ showResults(results);
591
+ const reportDir = join(targetDir, 'checklist_report');
592
+ mkdirSync(reportDir, { recursive: true });
593
+ const reportFile = join(reportDir, `assessment_${project}_${Date.now()}.json`);
594
+ writeFileSync(reportFile, JSON.stringify({
595
+ project, date: new Date().toISOString(),
596
+ total: results.length,
597
+ passed: results.filter(r => r.status === 'PASS').length,
598
+ failed: results.filter(r => r.status === 'FAIL').length,
599
+ manual: results.filter(r => r.status === 'MANUAL').length,
600
+ results
601
+ }, null, 2));
602
+ log(chalk.dim(` Report saved: ${reportFile}\n`));
603
+
604
+ const { genDiag } = await inquirer.prompt([{
605
+ type: 'confirm', name: 'genDiag',
606
+ message: 'Generate ERD & DFD Mermaid diagrams for this project?',
607
+ default: true
608
+ }]);
609
+ if (genDiag) {
610
+ const filePath = join(targetDir, `${project}_ERD_DFD.md`);
611
+ initMermaidFile(filePath, project);
612
+ log(chalk.green(` ✓ Diagrams saved to: ${chalk.bold(filePath)}\n`));
613
+ }
614
+ return;
615
+ }
616
+
617
+ // INSTALL mode
209
618
  const exist = [];
210
619
  if (existsSync(join(targetDir, 'backend-project'))) exist.push('backend-project');
211
620
  if (existsSync(join(targetDir, 'frontend-project'))) exist.push('frontend-project');
212
621
 
213
- let proceed = true;
214
622
  if (exist.length > 0) {
215
- log(chalk.yellow(` Warning: ${exist.join(', ')} already exists in this directory.`));
623
+ log(chalk.yellow(` Warning: ${exist.join(', ')} already exists.`));
216
624
  const { overwrite } = await inquirer.prompt([{
217
- type: 'confirm',
218
- name: 'overwrite',
219
- message: 'Overwrite existing files?',
220
- default: false
625
+ type: 'confirm', name: 'overwrite',
626
+ message: 'Overwrite existing files?', default: false
221
627
  }]);
222
- if (overwrite) {
223
- if (existsSync(join(targetDir, 'backend-project'))) fs.removeSync(join(targetDir, 'backend-project'));
224
- if (existsSync(join(targetDir, 'frontend-project'))) fs.removeSync(join(targetDir, 'frontend-project'));
225
- const sqlFiles = ['database.sql'];
226
- for (const f of sqlFiles) { const p = join(targetDir, f); if (existsSync(p)) fs.removeSync(p); }
227
- } else {
228
- proceed = false;
229
- log(chalk.yellow(' Operation cancelled.'));
230
- process.exit(0);
628
+ if (!overwrite) { log(chalk.yellow(' Cancelled.')); process.exit(0); }
629
+ for (const f of ['backend-project', 'frontend-project', 'database.sql']) {
630
+ const p = join(targetDir, f);
631
+ if (existsSync(p)) fs.removeSync(p);
231
632
  }
232
633
  }
233
634
 
234
- log(chalk.bold('\n Available Projects:\n'));
235
- for (const p of PROJECT_CHOICES) {
236
- log(` ${chalk.cyan(p.name)} ${chalk.dim('-')} ${p.description}`);
237
- }
238
-
239
- const { project } = await inquirer.prompt([{
240
- type: 'list',
241
- name: 'project',
242
- message: 'Select project to install:',
243
- choices: PROJECT_CHOICES.map(p => ({ name: `${p.name} - ${p.description}`, value: p }))
244
- }]);
245
-
246
- log(chalk.bold(`\n Installing ${project.name}...\n`));
635
+ const projectDir = project === 'SIMS' ? 'SIMS-master' : 'CRPMS-main';
636
+ const srcDir = join(PROJECTS_DIR, projectDir);
637
+ if (!existsSync(srcDir)) { log(chalk.red(` Error: Source ${srcDir} not found`)); process.exit(1); }
247
638
 
248
- const srcProjectDir = join(PROJECTS_DIR, project.dir);
249
- if (!existsSync(srcProjectDir)) {
250
- log(chalk.red(` Error: Project source not found at ${srcProjectDir}`));
251
- process.exit(1);
252
- }
253
-
254
- const spinnerChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
639
+ log(chalk.bold(`\n Installing ${project}...`));
640
+ const spinChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
255
641
  let si = 0;
256
- const spin = setInterval(() => { process.stdout.write(`\r ${chalk.cyan(spinnerChars[si++ % spinnerChars.length])} Copying project files...`); }, 80);
642
+ const spin = setInterval(() => { process.stdout.write(`\r ${chalk.cyan(spinChars[si++ % spinChars.length])} Installing...`); }, 80);
257
643
 
258
644
  try {
259
- const entries = readdirSync(srcProjectDir);
645
+ const entries = readdirSync(srcDir);
260
646
  for (const entry of entries) {
261
647
  if (entry === 'node_modules') continue;
262
- const srcPath = join(srcProjectDir, entry);
263
- const destPath = join(targetDir, entry);
264
- if (statSync(srcPath).isDirectory()) {
265
- fs.copySync(srcPath, destPath, { filter: (f) => !f.includes('node_modules') });
266
- } else {
267
- copyFileSync(srcPath, destPath);
268
- }
648
+ const s = join(srcDir, entry);
649
+ const d = join(targetDir, entry);
650
+ if (statSync(s).isDirectory()) fs.copySync(s, d, { filter: f => !f.includes('node_modules') });
651
+ else copyFileSync(s, d);
269
652
  }
270
- clearInterval(spin);
271
- process.stdout.write('\r \r');
272
- log(chalk.green(' ✓ Project files copied\n'));
273
653
 
274
- const backendDir = join(targetDir, 'backend-project');
275
- const frontendDir = join(targetDir, 'frontend-project');
654
+ const beDir = join(targetDir, 'backend-project');
655
+ const feDir = join(targetDir, 'frontend-project');
276
656
 
277
- if (existsSync(backendDir) && existsSync(join(backendDir, 'package.json'))) {
278
- await runInstall(backendDir, 'Backend');
657
+ if (existsSync(beDir) && existsSync(join(beDir, 'package.json'))) {
658
+ process.chdir(beDir);
659
+ log(chalk.cyan('\n Installing Backend dependencies...'));
660
+ try { await promisify(exec)('npm install', { timeout: 120000, windowsHide: true }); log(chalk.green(' ✓ Backend done')); } catch (e) { log(chalk.yellow(` ⚠ Backend npm install: ${e.message}`)); }
279
661
  }
280
- if (existsSync(frontendDir) && existsSync(join(frontendDir, 'package.json'))) {
281
- await runInstall(frontendDir, 'Frontend');
662
+ if (existsSync(feDir) && existsSync(join(feDir, 'package.json'))) {
663
+ process.chdir(feDir);
664
+ log(chalk.cyan(' Installing Frontend dependencies...'));
665
+ try { await promisify(exec)('npm install', { timeout: 120000, windowsHide: true }); log(chalk.green(' ✓ Frontend done')); } catch (e) { log(chalk.yellow(` ⚠ Frontend npm install: ${e.message}`)); }
282
666
  }
283
667
 
284
668
  process.chdir(targetDir);
669
+ clearInterval(spin);
670
+ process.stdout.write('\r \r');
671
+ log(chalk.green(' ✓ Project installed\n'));
285
672
 
286
- log(chalk.bold('\n ─── Database Setup ───\n'));
287
- const dbSqlPath = join(targetDir, 'database.sql');
288
- if (existsSync(dbSqlPath)) {
289
- log(chalk.dim(' Database SQL file found. Import instructions:\n'));
290
- log(` ${chalk.cyan(' 1) Open MySQL client (MySQL Workbench / phpMyAdmin / CLI')}`);
291
- log(` ${chalk.cyan(' 2) Execute the SQL script from:')}`);
292
- log(` ${chalk.dim(dbSqlPath)}`);
293
- log(` ${chalk.cyan(` 3) This will create the "${project.db}" database and all tables`)}\n`);
294
-
295
- const { doDb } = await inquirer.prompt([{
296
- type: 'confirm',
297
- name: 'doDb',
298
- message: 'Attempt automatic database import? (requires MySQL CLI)',
299
- default: false
300
- }]);
301
-
302
- if (doDb) {
303
- const dbSql = readFileSync(dbSqlPath, 'utf8');
304
- try {
305
- await execAsync('mysql -u root', { input: dbSql, timeout: 30000, windowsHide: true });
306
- log(chalk.green(' ✓ Database imported successfully'));
307
- } catch (err) {
308
- log(chalk.yellow(` ⚠ Could not auto-import: ${err.message}`));
309
- log(chalk.dim(' Please import manually using the SQL file.'));
310
- }
311
- }
673
+ if (existsSync(join(beDir, '.env.example')) && !existsSync(join(beDir, '.env'))) {
674
+ copyFileSync(join(beDir, '.env.example'), join(beDir, '.env'));
675
+ log(chalk.green(' ✓ .env created'));
312
676
  }
313
677
 
314
- if (existsSync(join(backendDir, '.env.example'))) {
315
- const envDest = join(backendDir, '.env');
316
- if (!existsSync(envDest)) {
317
- copyFileSync(join(backendDir, '.env.example'), envDest);
318
- log(chalk.green(' ✓ .env file created from .env.example'));
319
- }
678
+ log(chalk.bold('\n ─── QUICK START ───\n'));
679
+ log(` ${chalk.cyan('1. Import database:')} Run database.sql in MySQL`);
680
+ log(` ${chalk.cyan('2. Start backend:')} cd backend-project && npm start (port 5000)`);
681
+ log(` ${chalk.cyan('3. Start frontend:')} cd frontend-project && npm run dev (port 5173)`);
682
+ const dbName = project === 'SIMS' ? 'SIMS' : 'CRPMS';
683
+ log(` ${chalk.cyan('4. Open browser:')} http://localhost:5173`);
684
+ log(` ${chalk.cyan(' Login:')} ${project === 'SIMS' ? 'admin / admin123' : 'admin / Admin@1234'}`);
685
+ log(` ${chalk.cyan(' Database:')} ${dbName}\n`);
686
+
687
+ const { doAssess } = await inquirer.prompt([{
688
+ type: 'confirm', name: 'doAssess',
689
+ message: 'Run auto-assessment checklist now?', default: true
690
+ }]);
691
+
692
+ if (doAssess) {
693
+ const checks = buildChecklist(targetDir, project);
694
+ const results = checks.map(c => {
695
+ let status;
696
+ try {
697
+ const result = c.auto();
698
+ status = result === true || result === 'PASS' ? 'PASS' : (result === 'MANUAL' ? 'MANUAL' : 'FAIL');
699
+ } catch { status = 'MANUAL'; }
700
+ return { ...c, status, guide: c.guide || '' };
701
+ });
702
+ showResults(results);
703
+ const reportDir = join(targetDir, 'checklist_report');
704
+ mkdirSync(reportDir, { recursive: true });
705
+ const reportFile = join(reportDir, `assessment_${project}_${Date.now()}.json`);
706
+ writeFileSync(reportFile, JSON.stringify({
707
+ project, date: new Date().toISOString(),
708
+ total: results.length,
709
+ passed: results.filter(r => r.status === 'PASS').length,
710
+ failed: results.filter(r => r.status === 'FAIL').length,
711
+ manual: results.filter(r => r.status === 'MANUAL').length,
712
+ results
713
+ }, null, 2));
714
+ log(chalk.dim(` Report saved: ${reportFile}\n`));
320
715
  }
321
716
 
322
- log(chalk.bold('\n══════════════════════════════════════════════'));
323
- log(chalk.bold(' INSTALLATION SUMMARY'));
324
- log(chalk.bold('══════════════════════════════════════════════\n'));
325
- log(` ${chalk.cyan('Project:')} ${project.name}`);
326
- log(` ${chalk.cyan('Database:')} ${project.db}`);
327
- log(` ${chalk.cyan('Default User:')} ${project.defaultUser}`);
328
- log(` ${chalk.cyan('Backend:')} cd backend-project && npm start (port 5000)`);
329
- log(` ${chalk.cyan('Frontend:')} cd frontend-project && npm run dev (port 5173)`);
330
- log(chalk.dim(`\n ${targetDir}\n`));
331
-
332
- const { runCheck } = await inquirer.prompt([{
333
- type: 'confirm',
334
- name: 'runCheck',
335
- message: 'Run the NESA assessment checklist now?',
336
- default: true
717
+ const { genDiag } = await inquirer.prompt([{
718
+ type: 'confirm', name: 'genDiag',
719
+ message: 'Generate ERD & DFD Mermaid diagrams?', default: true
337
720
  }]);
338
-
339
- if (runCheck) {
340
- await runChecklist(project.name);
721
+ if (genDiag) {
722
+ const filePath = join(targetDir, `${project}_ERD_DFD.md`);
723
+ initMermaidFile(filePath, project);
724
+ log(chalk.green(` ✓ Diagrams saved to: ${chalk.bold(filePath)}\n`));
341
725
  }
342
726
 
343
- log(chalk.bold.green('\n ✓ Installation complete!'));
344
- log(`\n ${chalk.dim('To start the backend:')}`);
345
- log(` ${chalk.cyan(' cd backend-project && npm start')}`);
346
- log(`\n ${chalk.dim('To start the frontend (new terminal):')}`);
347
- log(` ${chalk.cyan(' cd frontend-project && npm run dev')}`);
348
- log(`\n ${chalk.dim('Open:')} ${chalk.cyan('http://localhost:5173')}\n`);
727
+ log(chalk.bold.green('\n ✓ All done!\n'));
349
728
 
350
729
  } catch (err) {
351
730
  clearInterval(spin);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npms-exam-kit",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "NESA National Practical Exam Projects Installer - SIMS & CRPMS",
5
5
  "bin": "bin/exam-kit.js",
6
6
  "keywords": [
@@ -15,7 +15,6 @@
15
15
  "dependencies": {
16
16
  "chalk": "^5.3.0",
17
17
  "inquirer": "^9.2.0",
18
- "ora": "^8.0.0",
19
18
  "fs-extra": "^11.2.0"
20
19
  },
21
20
  "type": "module",
@@ -24,7 +23,6 @@
24
23
  },
25
24
  "files": [
26
25
  "bin/",
27
- "src/",
28
26
  "projects/"
29
27
  ]
30
28
  }