nibula 1.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 (81) hide show
  1. package/.eleventy.js +96 -0
  2. package/.eleventyignore +4 -0
  3. package/CHANGELOG.md +0 -0
  4. package/LICENSE +170 -0
  5. package/NOTICE +5 -0
  6. package/README.md +105 -0
  7. package/_tools/assistant.js +152 -0
  8. package/_tools/buildJs.js +37 -0
  9. package/_tools/cleanOutput.js +25 -0
  10. package/_tools/modules/constants.js +66 -0
  11. package/_tools/modules/pageComponents.js +77 -0
  12. package/_tools/modules/updateData.js +90 -0
  13. package/_tools/modules/updateOutputPath.js +112 -0
  14. package/_tools/modules/updatePage.js +162 -0
  15. package/_tools/modules/utils.js +27 -0
  16. package/_tools/modules/validation.js +30 -0
  17. package/_tools/res/templates/template.js +5 -0
  18. package/_tools/res/templates/template.njk +9 -0
  19. package/_tools/res/templates/template.scss +23 -0
  20. package/_tools/res/templates/template.ts +5 -0
  21. package/bin/create.js +407 -0
  22. package/bin/nibula.js +281 -0
  23. package/docs/Assistant CLI.md +66 -0
  24. package/docs/Backend.md +151 -0
  25. package/docs/Components.md +96 -0
  26. package/docs/Creating pages.md +65 -0
  27. package/docs/Deploy.md +101 -0
  28. package/docs/Head and SEO.md +117 -0
  29. package/docs/Javascript.md +53 -0
  30. package/docs/Styling with SCSS.md +136 -0
  31. package/nginx.conf +47 -0
  32. package/nibula-1.0.0.tgz +0 -0
  33. package/package.json +74 -0
  34. package/src/backend/.htaccess +7 -0
  35. package/src/backend/_core/composer.json +5 -0
  36. package/src/backend/_core/composer.lock +492 -0
  37. package/src/backend/_core/index.php +148 -0
  38. package/src/backend/_core/init.php +34 -0
  39. package/src/backend/_core/modules/RateLimiter.php +31 -0
  40. package/src/backend/_core/modules/Response.php +49 -0
  41. package/src/backend/api/protected/example-protected.php +17 -0
  42. package/src/backend/api/public/example-public.php +17 -0
  43. package/src/backend/database/Database.php +24 -0
  44. package/src/backend/database/migrations/create_example_db.sql +1 -0
  45. package/src/backend/example.config.php +28 -0
  46. package/src/backend/web.config +17 -0
  47. package/src/frontend/.htaccess +16 -0
  48. package/src/frontend/404.njk +17 -0
  49. package/src/frontend/assets/brand/favicon.svg +37 -0
  50. package/src/frontend/assets/brand/logo.svg +37 -0
  51. package/src/frontend/components/global/footer.njk +25 -0
  52. package/src/frontend/components/global/header.njk +7 -0
  53. package/src/frontend/components/welcome.njk +116 -0
  54. package/src/frontend/data/site.json +54 -0
  55. package/src/frontend/index.njk +9 -0
  56. package/src/frontend/js/modules/exampleModule.js +3 -0
  57. package/src/frontend/js/pages/404.js +7 -0
  58. package/src/frontend/js/pages/homepage.js +7 -0
  59. package/src/frontend/layouts/base.njk +142 -0
  60. package/src/frontend/layouts/page-components.njk +14 -0
  61. package/src/frontend/llms.njk +18 -0
  62. package/src/frontend/robots.njk +8 -0
  63. package/src/frontend/scss/modules/_animations.scss +25 -0
  64. package/src/frontend/scss/modules/_footer.scss +28 -0
  65. package/src/frontend/scss/modules/_global.scss +44 -0
  66. package/src/frontend/scss/modules/_header.scss +28 -0
  67. package/src/frontend/scss/modules/_mobile.scss +30 -0
  68. package/src/frontend/scss/modules/_root.scss +35 -0
  69. package/src/frontend/scss/modules/_typography.scss +15 -0
  70. package/src/frontend/scss/modules/frameworks/_bootstrap.scss +110 -0
  71. package/src/frontend/scss/modules/frameworks/_bulma.scss +109 -0
  72. package/src/frontend/scss/modules/frameworks/_foundation.scss +139 -0
  73. package/src/frontend/scss/modules/frameworks/_uikit.scss +110 -0
  74. package/src/frontend/scss/pages/404.scss +28 -0
  75. package/src/frontend/scss/pages/homepage.scss +23 -0
  76. package/src/frontend/sitemap.njk +18 -0
  77. package/src/frontend/ts/modules/exampleModule.ts +3 -0
  78. package/src/frontend/ts/pages/404.ts +7 -0
  79. package/src/frontend/ts/pages/homepage.ts +7 -0
  80. package/src/frontend/web.config +27 -0
  81. package/tsconfig.json +25 -0
package/bin/create.js ADDED
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const { writeSync } = require('fs');
7
+ const { spawnSync } = require('child_process');
8
+ const { color } = require('../_tools/modules/constants');
9
+
10
+ // ── PATHS ────────────────────────────────────────────────────────────────────
11
+
12
+ const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
13
+ const templateDir = path.join(__dirname, '..');
14
+ const SELF_VERSION = require('../package.json').version;
15
+
16
+ // ── ENUMS ────────────────────────────────────────────────────────────────────
17
+
18
+ const LANGUAGE = Object.freeze({
19
+ JAVASCRIPT: 'javascript',
20
+ TYPESCRIPT: 'typescript',
21
+ });
22
+
23
+ const FRAMEWORK = Object.freeze({
24
+ BOOTSTRAP: 'bootstrap',
25
+ BULMA: 'bulma',
26
+ FOUNDATION: 'foundation',
27
+ UIKIT: 'uikit',
28
+ NONE: 'none',
29
+ });
30
+
31
+ // ── CHOICES ──────────────────────────────────────────────────────────────────
32
+
33
+ const LANGUAGE_CHOICES = [
34
+ { label: 'JavaScript (recomened)', value: LANGUAGE.JAVASCRIPT },
35
+ { label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
36
+ ];
37
+
38
+ const FRAMEWORK_CHOICES = [
39
+ { label: 'Bootstrap (recomended)', value: FRAMEWORK.BOOTSTRAP },
40
+ { label: 'Bulma', value: FRAMEWORK.BULMA },
41
+ { label: 'Foundation', value: FRAMEWORK.FOUNDATION },
42
+ { label: 'UIkit', value: FRAMEWORK.UIKIT },
43
+ { label: 'None', value: FRAMEWORK.NONE },
44
+ ];
45
+
46
+ // ── COPY CONFIG ───────────────────────────────────────────────────────────────
47
+
48
+ const MANDATORY_COPY = [
49
+ '.eleventy.js',
50
+ '.eleventyignore',
51
+ 'nginx.conf',
52
+ 'src/backend',
53
+ 'src/frontend',
54
+ ];
55
+
56
+ const FRONTEND_EXCLUDE = {
57
+ [LANGUAGE.JAVASCRIPT]: ['ts'],
58
+ [LANGUAGE.TYPESCRIPT]: ['js'],
59
+ };
60
+
61
+ const CREATE_DIRS = [
62
+ 'src/frontend/_routes',
63
+ ];
64
+
65
+ // ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
66
+
67
+ const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
68
+
69
+ const FRAMEWORKS = {
70
+ [FRAMEWORK.BOOTSTRAP]: {
71
+ scss: 'bootstrap',
72
+ njk: ['<script src="/js/bootstrap.bundle.min.js" defer></script>'],
73
+ eleventy: [
74
+ '"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
75
+ '"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
76
+ ],
77
+ },
78
+ [FRAMEWORK.BULMA]: {
79
+ scss: 'bulma',
80
+ njk: [],
81
+ eleventy: [],
82
+ },
83
+ [FRAMEWORK.FOUNDATION]: {
84
+ scss: 'foundation',
85
+ njk: ['<script src="/js/foundation.min.js" defer></script>'],
86
+ eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
87
+ },
88
+ [FRAMEWORK.UIKIT]: {
89
+ scss: 'uikit',
90
+ njk: [
91
+ '<script src="/js/uikit.min.js" defer></script>',
92
+ '<script src="/js/uikit-icons.min.js" defer></script>',
93
+ ],
94
+ eleventy: [
95
+ '"node_modules/uikit/dist/js/uikit.min.js": "js/uikit.min.js",',
96
+ '"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
97
+ ],
98
+ },
99
+ [FRAMEWORK.NONE]: {
100
+ scss: null,
101
+ njk: [],
102
+ eleventy: [],
103
+ },
104
+ };
105
+
106
+ // ── LANGUAGE CONFIG ───────────────────────────────────────────────────────────
107
+
108
+ const LANGUAGE_ELEVENTY = Object.freeze({
109
+ jsEntry: 'const entryPoints = glob.sync("src/frontend/js/pages/*.js");',
110
+ tsEntry: 'const entryPoints = glob.sync("src/frontend/ts/pages/*.ts");',
111
+ });
112
+
113
+ // ── GENERATED FILE CONTENTS ───────────────────────────────────────────────────
114
+
115
+ const GITIGNORE_CONTENT = `
116
+ node_modules/
117
+ src/backend/_core/vendor/
118
+ out/
119
+ src/backend/config.php
120
+ `;
121
+
122
+
123
+ const PROJECT_PACKAGE = {
124
+ name: path.basename(targetDir),
125
+ version: '1.0.0',
126
+ private: true,
127
+ outputDir: 'out',
128
+ "scripts": {
129
+ "build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
130
+ "build:js": "nib build-js",
131
+ "build:11ty": "eleventy",
132
+ "build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
133
+ "serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
134
+ "serve:js": "nib build-js --watch",
135
+ "serve:11ty": "eleventy --serve --quiet",
136
+ "clean": "nib clean",
137
+ "serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
138
+ "assistant": "nib cli"
139
+ },
140
+ dependencies: {
141
+ '@11ty/eleventy': '^3.1.2',
142
+ '@11ty/eleventy-img': '^6.0.4',
143
+ 'bootstrap': '^5.3.8',
144
+ 'bootstrap-icons': '^1.13.1',
145
+ 'bulma': '^1.0.4',
146
+ 'foundation-sites': '^6.9.0',
147
+ 'github-markdown-css': '^5.9.0',
148
+ 'glob': '^13.0.6',
149
+ 'uikit': '^3.25.13',
150
+ },
151
+ devDependencies: {
152
+ 'nibula': `^${SELF_VERSION}`,
153
+ 'concurrently': '^9.2.1',
154
+ 'esbuild': '^0.27.3',
155
+ 'sass': '^1.77.0',
156
+ },
157
+ };
158
+
159
+ // ── HELPERS ───────────────────────────────────────────────────────────────────
160
+
161
+ function log(msg) {
162
+ writeSync(1, msg + '\n');
163
+ }
164
+
165
+ function logAdd(name) {
166
+ log(`${color.green}+${color.reset} ${name}`);
167
+ }
168
+
169
+ function escapeRegex(str) {
170
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
171
+ }
172
+
173
+ function copyRecursive(src, dest, exclude = []) {
174
+ const stat = fs.statSync(src);
175
+ if (stat.isDirectory()) {
176
+ fs.mkdirSync(dest, { recursive: true });
177
+ for (const child of fs.readdirSync(src)) {
178
+ if (child === '.git') continue;
179
+ if (exclude.includes(child)) continue;
180
+ copyRecursive(path.join(src, child), path.join(dest, child), exclude);
181
+ }
182
+ } else {
183
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
184
+ fs.copyFileSync(src, dest);
185
+ }
186
+ }
187
+
188
+ function slashComment(content, line) {
189
+ content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
190
+ return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
191
+ }
192
+
193
+ function slashUncomment(content, line) {
194
+ return content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
195
+ }
196
+
197
+ function njkComment(content, line) {
198
+ content = content.split(`{# ${line} #}`).join(line);
199
+ return content.split(line).join(`{# ${line} #}`);
200
+ }
201
+
202
+ function njkUncomment(content, line) {
203
+ return content.split(`{# ${line} #}`).join(line);
204
+ }
205
+
206
+ function installDependencies() {
207
+ const backendCore = path.join(targetDir, 'src', 'backend', '_core');
208
+
209
+ log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
210
+ const npm = spawnSync('npm', ['install'], {
211
+ cwd: targetDir,
212
+ stdio: 'inherit',
213
+ shell: process.platform === 'win32',
214
+ });
215
+
216
+ if (npm.status !== 0) {
217
+ log('\n(!) npm install failed. Finish manually:');
218
+ if (process.argv[2]) log(` cd ${process.argv[2]}`);
219
+ log(' npm install\n');
220
+ return false;
221
+ }
222
+
223
+ if (!fs.existsSync(path.join(backendCore, 'composer.json'))) {
224
+ return true;
225
+ }
226
+
227
+ const probe = spawnSync('composer', ['--version'], {
228
+ stdio: 'ignore',
229
+ shell: process.platform === 'win32',
230
+ });
231
+
232
+ if (probe.status !== 0) {
233
+ log('\n(!) Composer not found — skipping backend dependencies.');
234
+ log(' Install Composer, then run: cd src/backend/_core && composer install\n');
235
+ return true;
236
+ }
237
+
238
+ log(`\n${color.blue}>> Installing Composer modules...${color.reset}\n`);
239
+ spawnSync('composer', ['install', '--quiet', '--no-interaction'], {
240
+ cwd: backendCore,
241
+ stdio: 'ignore',
242
+ shell: process.platform === 'win32',
243
+ });
244
+
245
+ return true;
246
+ }
247
+
248
+ // ── APPLY ─────────────────────────────────────────────────────────────────────
249
+
250
+ function applyFramework(framework) {
251
+ const config = FRAMEWORKS[framework];
252
+
253
+ const globalScssPath = path.join(targetDir, 'src/frontend/scss/modules/_global.scss');
254
+ if (fs.existsSync(globalScssPath)) {
255
+ let content = fs.readFileSync(globalScssPath, 'utf8');
256
+ ALL_FRAMEWORKS.forEach(fw => {
257
+ content = slashComment(content, `@import "../modules/frameworks/${fw}";`);
258
+ });
259
+ if (config.scss) {
260
+ content = slashUncomment(content, `@import "../modules/frameworks/${config.scss}";`);
261
+ }
262
+ fs.writeFileSync(globalScssPath, content);
263
+ }
264
+
265
+ const baseNjkPath = path.join(targetDir, 'src/frontend/layouts/base.njk');
266
+ if (fs.existsSync(baseNjkPath)) {
267
+ let content = fs.readFileSync(baseNjkPath, 'utf8');
268
+ ALL_FRAMEWORKS.forEach(fw => {
269
+ FRAMEWORKS[fw].njk.forEach(line => { content = njkComment(content, line); });
270
+ });
271
+ config.njk.forEach(line => { content = njkUncomment(content, line); });
272
+ fs.writeFileSync(baseNjkPath, content);
273
+ }
274
+
275
+ const eleventyPath = path.join(targetDir, '.eleventy.js');
276
+ if (fs.existsSync(eleventyPath)) {
277
+ let content = fs.readFileSync(eleventyPath, 'utf8');
278
+ ALL_FRAMEWORKS.forEach(fw => {
279
+ FRAMEWORKS[fw].eleventy.forEach(line => { content = slashComment(content, line); });
280
+ });
281
+ config.eleventy.forEach(line => { content = slashUncomment(content, line); });
282
+ fs.writeFileSync(eleventyPath, content);
283
+ }
284
+ }
285
+
286
+ function applyLanguage(language) {
287
+ const eleventyPath = path.join(targetDir, '.eleventy.js');
288
+ if (!fs.existsSync(eleventyPath)) return;
289
+
290
+ let content = fs.readFileSync(eleventyPath, 'utf8');
291
+
292
+ if (language === LANGUAGE.TYPESCRIPT) {
293
+ content = slashComment(content, LANGUAGE_ELEVENTY.jsEntry);
294
+ content = slashUncomment(content, LANGUAGE_ELEVENTY.tsEntry);
295
+ } else {
296
+ content = slashUncomment(content, LANGUAGE_ELEVENTY.jsEntry);
297
+ content = slashComment(content, LANGUAGE_ELEVENTY.tsEntry);
298
+ }
299
+
300
+ fs.writeFileSync(eleventyPath, content);
301
+ }
302
+
303
+ // ── UI ────────────────────────────────────────────────────────────────────────
304
+
305
+ function askChoice(question, choices) {
306
+ return new Promise((resolve) => {
307
+ let selectedIndex = 0;
308
+
309
+ log(`\n>> ${question} (Use arrow keys and press Enter):\n`);
310
+
311
+ const render = (firstTime = false) => {
312
+ if (!firstTime) process.stdout.write(`\x1B[${choices.length}A`);
313
+ const output = choices.map((choice, index) =>
314
+ index === selectedIndex
315
+ ? ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`
316
+ : ` * ${choice.label}\x1B[K\n`
317
+ ).join('');
318
+ process.stdout.write(output);
319
+ };
320
+
321
+ readline.emitKeypressEvents(process.stdin);
322
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
323
+ process.stdin.resume();
324
+
325
+ const onKeyPress = (str, key) => {
326
+ if (key.ctrl && key.name === 'c') {
327
+ process.exit();
328
+ } else if (key.name === 'up') {
329
+ selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : choices.length - 1;
330
+ render();
331
+ } else if (key.name === 'down') {
332
+ selectedIndex = selectedIndex < choices.length - 1 ? selectedIndex + 1 : 0;
333
+ render();
334
+ } else if (key.name === 'return' || key.name === 'enter') {
335
+ process.stdin.removeListener('keypress', onKeyPress);
336
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
337
+ process.stdin.pause();
338
+ resolve(choices[selectedIndex].value);
339
+ }
340
+ };
341
+
342
+ process.stdin.on('keypress', onKeyPress);
343
+ render(true);
344
+ });
345
+ }
346
+
347
+ // ── INIT ──────────────────────────────────────────────────────────────────────
348
+
349
+ async function init() {
350
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
351
+
352
+ log(`\n>> ${color.magenta}Creating Nibula project in ${targetDir}\n${color.reset}`);
353
+
354
+ const language = await askChoice('Select a language', LANGUAGE_CHOICES);
355
+ const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
356
+
357
+ log('');
358
+
359
+ for (const target of MANDATORY_COPY) {
360
+ const src = path.join(templateDir, target);
361
+ const dest = path.join(targetDir, target);
362
+ if (!fs.existsSync(src)) continue;
363
+ const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
364
+ copyRecursive(src, dest, exclude);
365
+ logAdd(target);
366
+ }
367
+
368
+ const configDest = path.join(targetDir, 'src/backend/config.php');
369
+ const configExample = path.join(targetDir, 'src/backend/example.config.php');
370
+ if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
371
+ fs.copyFileSync(configExample, configDest);
372
+ logAdd('src/backend/config.php');
373
+ }
374
+
375
+ const pkg = { ...PROJECT_PACKAGE };
376
+
377
+ if (language === LANGUAGE.TYPESCRIPT) {
378
+ const tsSrc = path.join(templateDir, 'tsconfig.json');
379
+ const tsDest = path.join(targetDir, 'tsconfig.json');
380
+ fs.copyFileSync(tsSrc, tsDest);
381
+ logAdd('tsconfig.json');
382
+ pkg.devDependencies = { ...pkg.devDependencies, typescript: 'latest' };
383
+ }
384
+
385
+ fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
386
+ logAdd('package.json');
387
+
388
+ fs.writeFileSync(path.join(targetDir, '.gitignore'), GITIGNORE_CONTENT);
389
+ logAdd('.gitignore');
390
+
391
+ for (const dir of CREATE_DIRS) {
392
+ fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
393
+ }
394
+
395
+ applyFramework(framework);
396
+ applyLanguage(language);
397
+
398
+ installDependencies();
399
+
400
+ log(`\n${color.green}>> Done!${color.reset}`);
401
+ log(`${color.yellow}\nNow run:\n${color.reset}`);
402
+ if (process.argv[2]) log(`${color.yellow}> cd ${process.argv[2]}${color.reset}`);
403
+ log(`${color.yellow}> nib run${color.reset}`);
404
+ log('');
405
+ }
406
+
407
+ init();
package/bin/nibula.js ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const https = require('https');
6
+ const readline = require('readline');
7
+ const { spawnSync } = require('child_process');
8
+ const { findProjectRoot, color, NOT_INSIDE_PROJECT_MESSAGE } = require('../_tools/modules/constants');
9
+
10
+ const pkg = require('../package.json');
11
+
12
+ const [, , cmd, ...rest] = process.argv;
13
+
14
+ const CREATE = path.join(__dirname, 'create.js');
15
+ const ASSISTANT = path.join(__dirname, '..', '_tools', 'assistant.js');
16
+ const BUILDJS = path.join(__dirname, '..', '_tools', 'buildJs.js');
17
+ const CLEAN = path.join(__dirname, '..', '_tools', 'cleanOutput.js');
18
+
19
+ const REGISTRY = 'https://registry.npmjs.org/nibula/latest';
20
+ const CHECK_TIMEOUT = 2500;
21
+
22
+ function run(script, args) {
23
+ const res = spawnSync('node', [script, ...args], {
24
+ stdio: 'inherit',
25
+ cwd: process.cwd(),
26
+ });
27
+ process.exit(res.status ?? 0);
28
+ }
29
+
30
+ function requireProjectRoot() {
31
+ const root = findProjectRoot(process.cwd());
32
+ if (!root) {
33
+ console.error(NOT_INSIDE_PROJECT_MESSAGE);
34
+ process.exit(1);
35
+ }
36
+ return root;
37
+ }
38
+
39
+ function maybeDelegateToLocal(root) {
40
+ const local = path.join(root, 'node_modules', 'nibula', 'bin', 'nibula.js');
41
+ if (!fs.existsSync(local)) return;
42
+
43
+ // Compare REAL paths so symlinks (e.g. from `npm link`) resolve to the same
44
+ // physical file; otherwise the guard would loop forever under npm link.
45
+ let localReal;
46
+ let selfReal;
47
+ try { localReal = fs.realpathSync(local); } catch { return; }
48
+ try { selfReal = fs.realpathSync(__filename); } catch { selfReal = __filename; }
49
+
50
+ if (localReal !== selfReal) {
51
+ const res = spawnSync('node', [localReal, ...process.argv.slice(2)], {
52
+ stdio: 'inherit',
53
+ cwd: process.cwd(),
54
+ });
55
+ process.exit(res.status ?? 0);
56
+ }
57
+ }
58
+
59
+ function enterProject() {
60
+ const root = requireProjectRoot();
61
+ maybeDelegateToLocal(root);
62
+ return root;
63
+ }
64
+
65
+ function runNpm(root, scriptName) {
66
+ const res = spawnSync('npm', ['run', scriptName], {
67
+ stdio: 'inherit',
68
+ cwd: root,
69
+ shell: process.platform === 'win32',
70
+ });
71
+ if (res.error) console.error(res.error.message);
72
+ process.exit(res.status ?? 0);
73
+ }
74
+
75
+ function fetchLatest() {
76
+ return new Promise((resolve) => {
77
+ const req = https.get(REGISTRY, (res) => {
78
+ if (res.statusCode !== 200) {
79
+ res.resume();
80
+ resolve(null);
81
+ return;
82
+ }
83
+ let data = '';
84
+ res.on('data', (chunk) => { data += chunk; });
85
+ res.on('end', () => {
86
+ try {
87
+ resolve(JSON.parse(data).version ?? null);
88
+ } catch {
89
+ resolve(null);
90
+ }
91
+ });
92
+ });
93
+ req.on('error', () => resolve(null));
94
+ req.setTimeout(CHECK_TIMEOUT, () => {
95
+ req.destroy();
96
+ resolve(null);
97
+ });
98
+ });
99
+ }
100
+
101
+ function isOlder(current, latest) {
102
+ const parse = (v) => String(v).split('-')[0].split('.').map(Number);
103
+ const a = parse(current);
104
+ const b = parse(latest);
105
+ for (let i = 0; i < 3; i++) {
106
+ const na = a[i] || 0;
107
+ const nb = b[i] || 0;
108
+ if (na < nb) return true;
109
+ if (na > nb) return false;
110
+ }
111
+ return false;
112
+ }
113
+
114
+ async function checkVersion() {
115
+ const latest = await fetchLatest();
116
+ return {
117
+ current: pkg.version,
118
+ latest,
119
+ behind: latest !== null && isOlder(pkg.version, latest),
120
+ };
121
+ }
122
+
123
+ function ask(question) {
124
+ return new Promise((resolve) => {
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
+ rl.question(question, (answer) => {
127
+ rl.close();
128
+ resolve(answer.trim().toLowerCase());
129
+ });
130
+ });
131
+ }
132
+
133
+ function updateGlobal(version) {
134
+ const target = version ? `nibula@${version}` : 'nibula@latest';
135
+ const res = spawnSync('npm', ['install', '-g', target, '--prefer-online'], {
136
+ stdio: 'inherit',
137
+ shell: process.platform === 'win32',
138
+ });
139
+ return res.status ?? 0;
140
+ }
141
+
142
+ function findExistingProject(baseDir, projectName) {
143
+ let entries;
144
+ try {
145
+ entries = fs.readdirSync(baseDir, { withFileTypes: true });
146
+ } catch {
147
+ return null;
148
+ }
149
+ for (const entry of entries) {
150
+ if (!entry.isDirectory()) continue;
151
+ const pkgPath = path.join(baseDir, entry.name, 'package.json');
152
+ if (!fs.existsSync(pkgPath)) continue;
153
+ try {
154
+ const data = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
155
+ if (data && data.name === projectName) {
156
+ return path.join(baseDir, entry.name);
157
+ }
158
+ } catch {
159
+ // ...
160
+ }
161
+ }
162
+ return null;
163
+ }
164
+
165
+ function usage(currentVersion) {
166
+ console.log(`
167
+ ${color.bold}${color.cyan}Nibula (${currentVersion})${color.reset} ${color.bold}by Michele Garofalo${color.reset}
168
+
169
+ ${color.yellow}nib new <project-name>${color.reset} Create your new project
170
+ ${color.yellow}nib cli${color.reset} Open the page-management assistant
171
+ ${color.yellow}nib run${color.reset} Start the dev server and builds out folder runtime
172
+ ${color.yellow}nib build${color.reset} Build the site out folder to publish
173
+ ${color.yellow}nib clean${color.reset} Remove the output directory
174
+ ${color.yellow}nib update${color.reset} Update to the latest version
175
+ `);
176
+ }
177
+
178
+ async function main() {
179
+ const info = await checkVersion();
180
+ switch (cmd) {
181
+ case 'new': {
182
+ if (!rest[0]) {
183
+ console.error('Missing project name. Usage: nib new <project-name>');
184
+ process.exit(1);
185
+ }
186
+ if (info.behind) {
187
+ console.log(`\nA newer version of Nibula is available: ${info.current} → ${info.latest}`);
188
+ if (process.stdin.isTTY) {
189
+ const answer = await ask('Update before creating the project? [Y/n] ');
190
+ if (answer === '' || answer === 'y' || answer === 'yes') {
191
+ const code = updateGlobal(info.latest);
192
+ if (code === 0) {
193
+ console.log(`\nUpdated. Re-run "nib new ${rest[0]}" to scaffold with the latest version.`);
194
+ }
195
+ process.exit(code);
196
+ }
197
+ } else {
198
+ console.log('Run "nib update" to update.\n');
199
+ }
200
+ }
201
+
202
+ // Controllo progetto già esistente con lo stesso "name" nel package.json
203
+ const existing = findExistingProject(process.cwd(), rest[0]);
204
+ if (existing) {
205
+ console.log(`\n${color.yellow}A project named "${rest[0]}" already exists:${color.reset} ${existing}`);
206
+ let overwrite = false;
207
+ if (process.stdin.isTTY) {
208
+ const answer = await ask('Do you want to overwrite it? [y/N] ');
209
+ overwrite = (answer === 'y' || answer === 'yes');
210
+ } else {
211
+ console.log('Run in a TTY to confirm overwrite.');
212
+ }
213
+ if (!overwrite) {
214
+ console.log('Stopped creating the new project');
215
+ process.exit(0);
216
+ }
217
+ // Sovrascrittura: cancello completamente la cartella preesistente
218
+ try {
219
+ fs.rmSync(existing, { recursive: true, force: true });
220
+ console.log(`${color.green}Removed existing project.${color.reset}`);
221
+ } catch (err) {
222
+ console.error(`${color.red}Failed to remove existing project:${color.reset} ${err.message}`);
223
+ process.exit(1);
224
+ }
225
+ }
226
+
227
+ run(CREATE, [rest[0]]);
228
+ break;
229
+ }
230
+ case 'cli': {
231
+ enterProject();
232
+ run(ASSISTANT, []);
233
+ break;
234
+ }
235
+ case 'run': {
236
+ const root = enterProject();
237
+ runNpm(root, 'serve');
238
+ break;
239
+ }
240
+ case 'build': {
241
+ const root = enterProject();
242
+ runNpm(root, 'build');
243
+ break;
244
+ }
245
+ case 'build-js': {
246
+ enterProject();
247
+ run(BUILDJS, rest);
248
+ break;
249
+ }
250
+ case 'clean': {
251
+ enterProject();
252
+ run(CLEAN, []);
253
+ break;
254
+ }
255
+ case 'update': {
256
+ if (info.latest !== null && !info.behind) {
257
+ console.log(`Already on the latest version (${info.current}).`);
258
+ process.exit(0);
259
+ }
260
+ process.exit(updateGlobal(info.latest));
261
+ }
262
+ case undefined:
263
+ case 'help':
264
+ case '-help':
265
+ case '--help':
266
+ case 'h':
267
+ case '-h': {
268
+ usage(info.current);
269
+ if (info.behind) {
270
+ console.log(`A newer version is available: ${info.current} → ${info.latest}. Run "nib update".`);
271
+ }
272
+ break;
273
+ }
274
+ default:
275
+ console.error(`${color.red}\nUnknown command:${color.reset} ${cmd}`);
276
+ usage(info.current);
277
+ process.exit(1);
278
+ }
279
+ }
280
+
281
+ main();