@tantainnovative/create-ndpr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # create-ndpr
2
+
3
+ CLI scaffolder for [NDPA](https://ndpr.gov.ng/) compliance using [`@tantainnovative/ndpr-toolkit`](https://github.com/tantainnovative/ndpr-toolkit).
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @tantainnovative/create-ndpr
9
+ ```
10
+
11
+ Or with the short alias (once published to npm):
12
+
13
+ ```bash
14
+ npx create-ndpr
15
+ ```
16
+
17
+ Run this from the root of an existing project. The CLI detects your stack and generates the right files with no manual copy-pasting.
18
+
19
+ ## What it does
20
+
21
+ 1. **Detects your project setup** — checks for `next.config.*`, `express` in `package.json`, an `app/` directory (App Router vs Pages Router), `prisma/schema.prisma`, and `drizzle.config.*`.
22
+
23
+ 2. **Prompts for a few details:**
24
+ - Organisation name
25
+ - DPO (Data Protection Officer) email address
26
+ - Framework (auto-detected, can override)
27
+ - ORM: Prisma / Drizzle / None
28
+ - Which compliance modules to include
29
+
30
+ 3. **Generates files** tailored to your stack.
31
+
32
+ ## What it generates
33
+
34
+ | File | When |
35
+ |------|------|
36
+ | `.env.example` | Always |
37
+ | `prisma/schema.prisma` | ORM = Prisma (skips if already exists) |
38
+ | `src/drizzle/ndpr-schema.ts` | ORM = Drizzle |
39
+ | `app/ndpr-layout.tsx` | Next.js App Router |
40
+ | `app/api/consent/route.ts` | Next.js + consent module |
41
+ | `app/api/dsr/route.ts` | Next.js + dsr module |
42
+ | `app/api/breach/route.ts` | Next.js + breach module |
43
+ | `pages/api/consent.ts` | Next.js Pages Router + consent |
44
+ | `pages/api/dsr.ts` | Next.js Pages Router + dsr |
45
+ | `pages/api/breach.ts` | Next.js Pages Router + breach |
46
+ | `src/ndpr/index.ts` | Express |
47
+ | `src/ndpr/routes/consent.ts` | Express + consent module |
48
+
49
+ All generated files use `{{ORG_NAME}}` and `{{DPO_EMAIL}}` substituted with your answers.
50
+
51
+ ## Modules
52
+
53
+ | Module | NDPA reference | Description |
54
+ |--------|---------------|-------------|
55
+ | `consent` | §25–26 | Consent collection, storage, and withdrawal |
56
+ | `dsr` | §34–38 | Data subject rights request intake and tracking |
57
+ | `breach` | §40 | 72-hour breach notification workflow |
58
+ | `policy` | — | Privacy policy scaffolding |
59
+ | `dpia` | — | Data Protection Impact Assessment |
60
+ | `lawful-basis` | §25 | Lawful basis register |
61
+ | `cross-border` | §43 | Cross-border data transfer management |
62
+ | `ropa` | Accountability | Record of Processing Activities |
63
+
64
+ ## After generation
65
+
66
+ ### With Prisma
67
+
68
+ ```bash
69
+ # Copy .env.example → .env and set DATABASE_URL
70
+ cp .env.example .env
71
+
72
+ # Install dependencies
73
+ pnpm add @prisma/client @tantainnovative/ndpr-toolkit
74
+ pnpm add -D prisma
75
+
76
+ # Run migrations
77
+ pnpm prisma migrate dev --name ndpr-init
78
+ ```
79
+
80
+ ### With Drizzle
81
+
82
+ ```bash
83
+ cp .env.example .env
84
+
85
+ pnpm add drizzle-orm @paralleldrive/cuid2 @tantainnovative/ndpr-toolkit
86
+ pnpm add -D drizzle-kit
87
+
88
+ pnpm drizzle-kit push
89
+ ```
90
+
91
+ ### Next.js — wire up the layout
92
+
93
+ ```tsx
94
+ // app/layout.tsx
95
+ import NDPRLayout from '@/app/ndpr-layout';
96
+
97
+ export default async function RootLayout({ children }) {
98
+ return (
99
+ <html lang="en">
100
+ <body>
101
+ <NDPRLayout>
102
+ {children}
103
+ </NDPRLayout>
104
+ </body>
105
+ </html>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ### Express — mount the router
111
+
112
+ ```ts
113
+ import express from 'express';
114
+ import { createNDPRRouter } from './src/ndpr';
115
+
116
+ const app = express();
117
+ app.use(express.json());
118
+ app.use('/api/ndpr', createNDPRRouter());
119
+ ```
120
+
121
+ ## Requirements
122
+
123
+ - Node.js 18+
124
+ - Zero external dependencies — works with `npx` out of the box
125
+
126
+ ## Links
127
+
128
+ - Toolkit docs: https://ndpr-toolkit.tantainnovative.com
129
+ - GitHub: https://github.com/tantainnovative/ndpr-toolkit
130
+ - NDPC: https://ndpr.gov.ng
package/bin/index.mjs ADDED
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from 'readline';
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ writeFileSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ statSync,
11
+ } from 'fs';
12
+ import { join, dirname, resolve } from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
17
+ const CWD = process.cwd();
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // ANSI colour helpers (no external deps)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const c = {
24
+ reset: '\x1b[0m',
25
+ bold: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ green: '\x1b[32m',
28
+ cyan: '\x1b[36m',
29
+ yellow: '\x1b[33m',
30
+ blue: '\x1b[34m',
31
+ magenta: '\x1b[35m',
32
+ red: '\x1b[31m',
33
+ white: '\x1b[37m',
34
+ };
35
+
36
+ const bold = (s) => `${c.bold}${s}${c.reset}`;
37
+ const green = (s) => `${c.green}${s}${c.reset}`;
38
+ const cyan = (s) => `${c.cyan}${s}${c.reset}`;
39
+ const yellow = (s) => `${c.yellow}${s}${c.reset}`;
40
+ const dim = (s) => `${c.dim}${s}${c.reset}`;
41
+ const red = (s) => `${c.red}${s}${c.reset}`;
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Banner
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function printBanner() {
48
+ console.log();
49
+ console.log(bold(cyan(' ███╗ ██╗██████╗ ██████╗ ██████╗ ')));
50
+ console.log(bold(cyan(' ████╗ ██║██╔══██╗██╔══██╗██╔══██╗')));
51
+ console.log(bold(cyan(' ██╔██╗ ██║██║ ██║██████╔╝██████╔╝')));
52
+ console.log(bold(cyan(' ██║╚██╗██║██║ ██║██╔═══╝ ██╔══██╗')));
53
+ console.log(bold(cyan(' ██║ ╚████║██████╔╝██║ ██║ ██║')));
54
+ console.log(bold(cyan(' ╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝ ╚═╝')));
55
+ console.log();
56
+ console.log(bold(' create-ndpr') + dim(' — NDPA compliance scaffolder'));
57
+ console.log(dim(' Powered by @tantainnovative/ndpr-toolkit'));
58
+ console.log();
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Stack detection
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function detectStack() {
66
+ const detected = {
67
+ framework: null, // 'nextjs-app' | 'nextjs-pages' | 'express' | null
68
+ orm: null, // 'prisma' | 'drizzle' | null
69
+ hasPackageJson: false,
70
+ };
71
+
72
+ // Check package.json exists
73
+ const pkgPath = join(CWD, 'package.json');
74
+ if (existsSync(pkgPath)) {
75
+ detected.hasPackageJson = true;
76
+ try {
77
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
78
+ const deps = {
79
+ ...((pkg.dependencies) || {}),
80
+ ...((pkg.devDependencies) || {}),
81
+ };
82
+
83
+ if (deps['express']) {
84
+ detected.framework = 'express';
85
+ }
86
+
87
+ if (deps['drizzle-orm'] || deps['drizzle-kit']) {
88
+ detected.orm = 'drizzle';
89
+ }
90
+ if (deps['@prisma/client'] || deps['prisma']) {
91
+ detected.orm = 'prisma';
92
+ }
93
+ } catch {
94
+ // ignore parse errors
95
+ }
96
+ }
97
+
98
+ // Next.js detection via config file
99
+ const nextConfigs = ['next.config.js', 'next.config.ts', 'next.config.mjs'];
100
+ const hasNextConfig = nextConfigs.some((f) => existsSync(join(CWD, f)));
101
+ if (hasNextConfig) {
102
+ // App Router vs Pages Router: check for app/ directory
103
+ const hasAppDir =
104
+ existsSync(join(CWD, 'app')) ||
105
+ existsSync(join(CWD, 'src', 'app'));
106
+ detected.framework = hasAppDir ? 'nextjs-app' : 'nextjs-pages';
107
+ }
108
+
109
+ // ORM detection via project structure (if not already found in deps)
110
+ if (!detected.orm) {
111
+ if (existsSync(join(CWD, 'prisma', 'schema.prisma'))) {
112
+ detected.orm = 'prisma';
113
+ } else {
114
+ const drizzleConfigs = ['drizzle.config.ts', 'drizzle.config.js', 'drizzle.config.mjs'];
115
+ if (drizzleConfigs.some((f) => existsSync(join(CWD, f)))) {
116
+ detected.orm = 'drizzle';
117
+ }
118
+ }
119
+ }
120
+
121
+ return detected;
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Readline helpers
126
+ // ---------------------------------------------------------------------------
127
+
128
+ function createRL() {
129
+ return createInterface({
130
+ input: process.stdin,
131
+ output: process.stdout,
132
+ });
133
+ }
134
+
135
+ function ask(rl, question) {
136
+ return new Promise((resolve) => {
137
+ rl.question(question, (answer) => resolve(answer.trim()));
138
+ });
139
+ }
140
+
141
+ async function askRequired(rl, question, label) {
142
+ let value = '';
143
+ while (!value) {
144
+ value = await ask(rl, question);
145
+ if (!value) {
146
+ console.log(red(` ${label} is required.`));
147
+ }
148
+ }
149
+ return value;
150
+ }
151
+
152
+ async function askChoice(rl, question, choices, defaultIndex = 0) {
153
+ const lines = choices.map((c, i) => ` ${i + 1}) ${c}`).join('\n');
154
+ const prompt = `${question}\n${lines}\n Choice [${defaultIndex + 1}]: `;
155
+ while (true) {
156
+ const raw = await ask(rl, prompt);
157
+ if (!raw) return defaultIndex;
158
+ const n = parseInt(raw, 10);
159
+ if (!isNaN(n) && n >= 1 && n <= choices.length) return n - 1;
160
+ console.log(yellow(` Please enter a number between 1 and ${choices.length}.`));
161
+ }
162
+ }
163
+
164
+ async function askCheckboxes(rl, question, options, defaults = []) {
165
+ const defaultStr = defaults.length ? ` [default: ${defaults.map((i) => i + 1).join(',')}]` : '';
166
+ const lines = options.map((o, i) => ` ${i + 1}) ${o}`).join('\n');
167
+ const prompt = `${question}${defaultStr}\n${lines}\n Enter numbers separated by commas (or press Enter for default): `;
168
+
169
+ while (true) {
170
+ const raw = await ask(rl, prompt);
171
+ if (!raw) return defaults;
172
+ const parts = raw.split(',').map((s) => s.trim()).filter(Boolean);
173
+ const indices = [];
174
+ let valid = true;
175
+ for (const p of parts) {
176
+ const n = parseInt(p, 10);
177
+ if (isNaN(n) || n < 1 || n > options.length) {
178
+ console.log(yellow(` Invalid option: ${p}. Enter numbers from 1-${options.length}.`));
179
+ valid = false;
180
+ break;
181
+ }
182
+ indices.push(n - 1);
183
+ }
184
+ if (valid) return [...new Set(indices)];
185
+ }
186
+ }
187
+
188
+ async function askYesNo(rl, question, defaultYes = true) {
189
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
190
+ const answer = await ask(rl, `${question} ${hint}: `);
191
+ if (!answer) return defaultYes;
192
+ return answer.toLowerCase().startsWith('y');
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Template rendering
197
+ // ---------------------------------------------------------------------------
198
+
199
+ function renderTemplate(templateName, vars) {
200
+ const templatePath = join(TEMPLATES_DIR, templateName);
201
+ if (!existsSync(templatePath)) {
202
+ throw new Error(`Template not found: ${templateName}`);
203
+ }
204
+ let content = readFileSync(templatePath, 'utf8');
205
+ for (const [key, value] of Object.entries(vars)) {
206
+ content = content.replaceAll(`{{${key}}}`, value);
207
+ }
208
+ return content;
209
+ }
210
+
211
+ function writeFile(filePath, content) {
212
+ const dir = dirname(filePath);
213
+ if (!existsSync(dir)) {
214
+ mkdirSync(dir, { recursive: true });
215
+ }
216
+ writeFileSync(filePath, content, 'utf8');
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // File generation
221
+ // ---------------------------------------------------------------------------
222
+
223
+ const GENERATED_FILES = [];
224
+
225
+ function generate(destRelative, templateName, vars = {}) {
226
+ const destPath = join(CWD, destRelative);
227
+ const content = renderTemplate(templateName, vars);
228
+ writeFile(destPath, content);
229
+ GENERATED_FILES.push(destRelative);
230
+ console.log(` ${green('+')} ${destRelative}`);
231
+ }
232
+
233
+ function generateRaw(destRelative, content) {
234
+ const destPath = join(CWD, destRelative);
235
+ writeFile(destPath, content);
236
+ GENERATED_FILES.push(destRelative);
237
+ console.log(` ${green('+')} ${destRelative}`);
238
+ }
239
+
240
+ function skip(destRelative, reason) {
241
+ console.log(` ${dim('-')} ${dim(destRelative)} ${dim(`(skipped — ${reason})`)}`);
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Main
246
+ // ---------------------------------------------------------------------------
247
+
248
+ async function main() {
249
+ printBanner();
250
+
251
+ // --- Detect stack ---
252
+ const detected = detectStack();
253
+
254
+ console.log(bold(' Detected project setup:'));
255
+ if (detected.framework) {
256
+ const label = {
257
+ 'nextjs-app': 'Next.js (App Router)',
258
+ 'nextjs-pages': 'Next.js (Pages Router)',
259
+ 'express': 'Express',
260
+ }[detected.framework];
261
+ console.log(` ${cyan('Framework:')} ${label}`);
262
+ } else {
263
+ console.log(` ${cyan('Framework:')} ${yellow('Not detected')}`);
264
+ }
265
+
266
+ if (detected.orm) {
267
+ console.log(` ${cyan('ORM:')} ${detected.orm === 'prisma' ? 'Prisma' : 'Drizzle'}`);
268
+ } else {
269
+ console.log(` ${cyan('ORM:')} ${dim('Not detected')}`);
270
+ }
271
+ console.log();
272
+
273
+ const rl = createRL();
274
+
275
+ try {
276
+ // --- Organisation details ---
277
+ console.log(bold(' Organisation details'));
278
+ console.log(dim(' These are embedded in the generated files.'));
279
+ console.log();
280
+
281
+ const orgName = await askRequired(
282
+ rl,
283
+ ` ${cyan('Organisation name')} (e.g. Acme Corp Nigeria Ltd): `,
284
+ 'Organisation name',
285
+ );
286
+
287
+ const dpoEmail = await askRequired(
288
+ rl,
289
+ ` ${cyan('DPO email address')} (e.g. dpo@acmecorp.ng): `,
290
+ 'DPO email',
291
+ );
292
+ console.log();
293
+
294
+ // --- Framework confirmation/override ---
295
+ const frameworkLabels = [
296
+ 'Next.js — App Router',
297
+ 'Next.js — Pages Router',
298
+ 'Express',
299
+ 'None (generate shared files only)',
300
+ ];
301
+ const frameworkValues = ['nextjs-app', 'nextjs-pages', 'express', 'none'];
302
+
303
+ let detectedFrameworkIdx = frameworkValues.indexOf(detected.framework ?? 'none');
304
+ if (detectedFrameworkIdx === -1) detectedFrameworkIdx = 3;
305
+
306
+ console.log(bold(' Framework'));
307
+ const frameworkIdx = await askChoice(
308
+ rl,
309
+ ` ${cyan('Which framework are you using?')}`,
310
+ frameworkLabels,
311
+ detectedFrameworkIdx,
312
+ );
313
+ const framework = frameworkValues[frameworkIdx];
314
+ console.log();
315
+
316
+ // --- Modules ---
317
+ const moduleOptions = [
318
+ 'consent — NDPA §25-26 consent management',
319
+ 'dsr — Data Subject Rights requests (§34-38)',
320
+ 'breach — Breach notification workflow (§40)',
321
+ 'policy — Privacy policy generation',
322
+ 'dpia — Data Protection Impact Assessment',
323
+ 'lawful-basis — Lawful basis register',
324
+ 'cross-border — Cross-border transfer management',
325
+ 'ropa — Record of Processing Activities',
326
+ ];
327
+ const moduleValues = ['consent', 'dsr', 'breach', 'policy', 'dpia', 'lawful-basis', 'cross-border', 'ropa'];
328
+
329
+ console.log(bold(' Compliance modules'));
330
+ const selectedModuleIndices = await askCheckboxes(
331
+ rl,
332
+ ` ${cyan('Which modules do you want to include?')}`,
333
+ moduleOptions,
334
+ [0, 1, 2], // consent, dsr, breach by default
335
+ );
336
+ const selectedModules = selectedModuleIndices.map((i) => moduleValues[i]);
337
+ console.log();
338
+
339
+ // --- ORM ---
340
+ const ormLabels = ['Prisma', 'Drizzle', 'None (skip database schema)'];
341
+ const ormValues = ['prisma', 'drizzle', 'none'];
342
+
343
+ let detectedOrmIdx = ormValues.indexOf(detected.orm ?? 'none');
344
+ if (detectedOrmIdx === -1) detectedOrmIdx = 0;
345
+
346
+ console.log(bold(' Database / ORM'));
347
+ const ormIdx = await askChoice(
348
+ rl,
349
+ ` ${cyan('Which ORM are you using?')}`,
350
+ ormLabels,
351
+ detectedOrmIdx,
352
+ );
353
+ const orm = ormValues[ormIdx];
354
+ console.log();
355
+
356
+ // --- Confirm ---
357
+ console.log(bold(' Summary'));
358
+ console.log(` ${cyan('Organisation:')} ${orgName}`);
359
+ console.log(` ${cyan('DPO email:')} ${dpoEmail}`);
360
+ console.log(` ${cyan('Framework:')} ${frameworkLabels[frameworkIdx]}`);
361
+ console.log(` ${cyan('ORM:')} ${ormLabels[ormIdx]}`);
362
+ console.log(` ${cyan('Modules:')} ${selectedModules.join(', ')}`);
363
+ console.log();
364
+
365
+ const confirmed = await askYesNo(rl, ` ${cyan('Generate files?')}`, true);
366
+ if (!confirmed) {
367
+ console.log(yellow('\n Aborted. No files were written.\n'));
368
+ process.exit(0);
369
+ }
370
+ console.log();
371
+
372
+ // -------------------------------------------------------------------------
373
+ // File generation
374
+ // -------------------------------------------------------------------------
375
+
376
+ console.log(bold(' Generating files...'));
377
+ console.log();
378
+
379
+ const vars = {
380
+ ORG_NAME: orgName,
381
+ DPO_EMAIL: dpoEmail,
382
+ };
383
+
384
+ // .env.example
385
+ generate('.env.example', 'env-example', vars);
386
+
387
+ // ORM schema
388
+ if (orm === 'prisma') {
389
+ if (existsSync(join(CWD, 'prisma', 'schema.prisma'))) {
390
+ skip('prisma/schema.prisma', 'already exists — merge the NDPR models manually');
391
+ console.log(dim(` See: ${TEMPLATES_DIR}/prisma-schema.prisma`));
392
+ } else {
393
+ generate('prisma/schema.prisma', 'prisma-schema.prisma', vars);
394
+ }
395
+ } else if (orm === 'drizzle') {
396
+ if (existsSync(join(CWD, 'src', 'drizzle', 'ndpr-schema.ts'))) {
397
+ skip('src/drizzle/ndpr-schema.ts', 'already exists');
398
+ } else {
399
+ generate('src/drizzle/ndpr-schema.ts', 'drizzle-schema.ts', vars);
400
+ }
401
+ }
402
+
403
+ // Framework-specific files
404
+ if (framework === 'nextjs-app') {
405
+ const appDir = existsSync(join(CWD, 'src', 'app')) ? 'src/app' : 'app';
406
+
407
+ // Layout wrapper
408
+ generate(`${appDir}/ndpr-layout.tsx`, 'nextjs-layout.tsx', vars);
409
+
410
+ // API routes per selected module
411
+ if (selectedModules.includes('consent')) {
412
+ generate(`${appDir}/api/consent/route.ts`, 'nextjs-consent-route.ts', vars);
413
+ }
414
+ if (selectedModules.includes('dsr')) {
415
+ generate(`${appDir}/api/dsr/route.ts`, 'nextjs-dsr-route.ts', vars);
416
+ }
417
+ if (selectedModules.includes('breach')) {
418
+ generate(`${appDir}/api/breach/route.ts`, 'nextjs-breach-route.ts', vars);
419
+ }
420
+ } else if (framework === 'nextjs-pages') {
421
+ console.log(yellow(' Note: Pages Router API routes generated under pages/api/'));
422
+ if (selectedModules.includes('consent')) {
423
+ generate('pages/api/consent.ts', 'nextjs-consent-route.ts', vars);
424
+ }
425
+ if (selectedModules.includes('dsr')) {
426
+ generate('pages/api/dsr.ts', 'nextjs-dsr-route.ts', vars);
427
+ }
428
+ if (selectedModules.includes('breach')) {
429
+ generate('pages/api/breach.ts', 'nextjs-breach-route.ts', vars);
430
+ }
431
+ } else if (framework === 'express') {
432
+ generate('src/ndpr/index.ts', 'express-setup.ts', vars);
433
+ if (selectedModules.includes('consent')) {
434
+ generate('src/ndpr/routes/consent.ts', 'express-consent-route.ts', vars);
435
+ }
436
+ }
437
+
438
+ // -------------------------------------------------------------------------
439
+ // Summary
440
+ // -------------------------------------------------------------------------
441
+
442
+ console.log();
443
+ console.log(bold(green(' Done! Files generated:')));
444
+ for (const f of GENERATED_FILES) {
445
+ console.log(` ${green('✓')} ${f}`);
446
+ }
447
+ console.log();
448
+
449
+ // Next steps
450
+ console.log(bold(' Next steps:'));
451
+ console.log();
452
+
453
+ if (orm === 'prisma') {
454
+ console.log(` ${cyan('1.')} Set your database URL in .env:`);
455
+ console.log(dim(' DATABASE_URL="postgresql://user:password@localhost:5432/mydb_dev"'));
456
+ console.log();
457
+ console.log(` ${cyan('2.')} Install the Prisma client and run migrations:`);
458
+ console.log(dim(' pnpm add @prisma/client'));
459
+ console.log(dim(' pnpm add -D prisma'));
460
+ console.log(dim(' pnpm prisma migrate dev --name ndpr-init'));
461
+ console.log();
462
+ } else if (orm === 'drizzle') {
463
+ console.log(` ${cyan('1.')} Set your database URL in .env:`);
464
+ console.log(dim(' DATABASE_URL="postgresql://user:password@localhost:5432/mydb_dev"'));
465
+ console.log();
466
+ console.log(` ${cyan('2.')} Install Drizzle and push the schema:`);
467
+ console.log(dim(' pnpm add drizzle-orm @paralleldrive/cuid2'));
468
+ console.log(dim(' pnpm add -D drizzle-kit'));
469
+ console.log(dim(' pnpm drizzle-kit push'));
470
+ console.log();
471
+ }
472
+
473
+ console.log(` ${cyan(`${orm === 'none' ? '1' : '3'}`.padStart(1))}. Install the ndpr-toolkit:`);
474
+ console.log(dim(' pnpm add @tantainnovative/ndpr-toolkit'));
475
+ console.log();
476
+
477
+ if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
478
+ const appDir = existsSync(join(CWD, 'src', 'app')) ? 'src/app' : 'app';
479
+ console.log(` ${cyan(`${orm === 'none' ? '2' : '4'}`.padStart(1))}. Wrap your root layout with NDPRLayout:`);
480
+ console.log(dim(` // ${appDir}/layout.tsx`));
481
+ console.log(dim(` import NDPRLayout from './${appDir.includes('src') ? '' : ''}ndpr-layout';`));
482
+ console.log(dim(' // Render <NDPRLayout>{children}</NDPRLayout> inside your <body>'));
483
+ console.log();
484
+ } else if (framework === 'express') {
485
+ console.log(` ${cyan(`${orm === 'none' ? '2' : '4'}`.padStart(1))}. Mount the NDPR router in your Express app:`);
486
+ console.log(dim(" import { createNDPRRouter } from './src/ndpr';"));
487
+ console.log(dim(" app.use('/api/ndpr', createNDPRRouter());"));
488
+ console.log();
489
+ }
490
+
491
+ console.log(dim(' Documentation: https://ndpr-toolkit.tantainnovative.com'));
492
+ console.log(dim(' Repository: https://github.com/tantainnovative/ndpr-toolkit'));
493
+ console.log();
494
+
495
+ } finally {
496
+ rl.close();
497
+ }
498
+ }
499
+
500
+ main().catch((err) => {
501
+ console.error(red(`\n Error: ${err.message}\n`));
502
+ process.exit(1);
503
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@tantainnovative/create-ndpr",
3
+ "version": "0.1.0",
4
+ "description": "CLI scaffolder for NDPA compliance with @tantainnovative/ndpr-toolkit",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Abraham Esandayinze Tanta",
8
+ "url": "https://linkedin.com/in/mr-tanta"
9
+ },
10
+ "bin": {
11
+ "create-ndpr": "./bin/index.mjs"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "templates/"
16
+ ],
17
+ "keywords": [
18
+ "ndpa",
19
+ "ndpr",
20
+ "cli",
21
+ "scaffold",
22
+ "nigeria",
23
+ "data-protection"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }